diff --git a/pkg/plugin/implementation/dediregistry/README.md b/pkg/plugin/implementation/dediregistry/README.md index e302559..0ba4e83 100644 --- a/pkg/plugin/implementation/dediregistry/README.md +++ b/pkg/plugin/implementation/dediregistry/README.md @@ -18,6 +18,7 @@ registry: config: url: "https://dedi-wrapper.example.com/dedi" registryName: "subscribers.beckn.one" + allowedParentNamespaces: "commerce-network.org,retail-collective.org" timeout: 30 retry_max: 3 retry_wait_min: 1s @@ -30,6 +31,7 @@ registry: |-----------|----------|-------------|---------| | `url` | Yes | DeDi wrapper API base URL (include /dedi path) | - | | `registryName` | Yes | Registry name for lookup path | - | +| `allowedParentNamespaces` | No | Allowlist of parent namespace domains for signature validation | - | | `timeout` | No | Request timeout in seconds | Client default | | `retry_max` | No | Maximum number of retry attempts | 4 (library default) | | `retry_wait_min` | No | Minimum wait time between retries (e.g., "1s", "500ms") | 1s (library default) | @@ -62,6 +64,7 @@ GET {url}/lookup/{subscriber_id}/{registryName}/{key_id} "signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=", "encr_public_key": "test-encr-key" }, + "parent_namespaces": ["commerce-network.org", "local-commerce.org"], "created_at": "2025-10-27T11:45:27.963Z", "updated_at": "2025-10-27T11:46:23.563Z" } @@ -158,4 +161,4 @@ This plugin replaces direct DeDi API integration with the new DeDi Wrapper API f - **Network Errors**: Connection failures, timeouts - **HTTP Errors**: Non-200 status codes from DeDi wrapper - **Data Errors**: Missing required fields in response -- **Validation Errors**: Empty subscriber ID or key ID in request \ No newline at end of file +- **Validation Errors**: Empty subscriber ID or key ID in request diff --git a/pkg/plugin/implementation/dediregistry/cmd/plugin.go b/pkg/plugin/implementation/dediregistry/cmd/plugin.go index 050a55d..5a0e31f 100644 --- a/pkg/plugin/implementation/dediregistry/cmd/plugin.go +++ b/pkg/plugin/implementation/dediregistry/cmd/plugin.go @@ -4,6 +4,7 @@ import ( "context" "errors" "strconv" + "strings" "github.com/beckn-one/beckn-onix/pkg/log" "github.com/beckn-one/beckn-onix/pkg/plugin/definition" @@ -34,6 +35,10 @@ func (d dediRegistryProvider) New(ctx context.Context, config map[string]string) } } + if rawNamespaces, exists := config["allowedParentNamespaces"]; exists && rawNamespaces != "" { + dediConfig.AllowedParentNamespaces = parseAllowedParentNamespaces(rawNamespaces) + } + log.Debugf(ctx, "DeDi Registry config mapped: %+v", dediConfig) dediClient, closer, err := dediregistry.New(ctx, dediConfig) @@ -46,5 +51,18 @@ func (d dediRegistryProvider) New(ctx context.Context, config map[string]string) return dediClient, closer, nil } +func parseAllowedParentNamespaces(raw string) []string { + parts := strings.Split(raw, ",") + namespaces := make([]string, 0, len(parts)) + for _, part := range parts { + item := strings.TrimSpace(part) + if item == "" { + continue + } + namespaces = append(namespaces, item) + } + return namespaces +} + // Provider is the exported plugin instance var Provider = dediRegistryProvider{} diff --git a/pkg/plugin/implementation/dediregistry/dediregistry.go b/pkg/plugin/implementation/dediregistry/dediregistry.go index 8bc4853..d13a1e6 100644 --- a/pkg/plugin/implementation/dediregistry/dediregistry.go +++ b/pkg/plugin/implementation/dediregistry/dediregistry.go @@ -15,12 +15,13 @@ import ( // Config holds configuration parameters for the DeDi registry client. type Config struct { - URL string `yaml:"url" json:"url"` - RegistryName string `yaml:"registryName" json:"registryName"` - Timeout int `yaml:"timeout" json:"timeout"` - RetryMax int `yaml:"retry_max" json:"retry_max"` - RetryWaitMin time.Duration `yaml:"retry_wait_min" json:"retry_wait_min"` - RetryWaitMax time.Duration `yaml:"retry_wait_max" json:"retry_wait_max"` + URL string `yaml:"url" json:"url"` + RegistryName string `yaml:"registryName" json:"registryName"` + AllowedParentNamespaces []string `yaml:"allowedParentNamespaces" json:"allowedParentNamespaces"` + Timeout int `yaml:"timeout" json:"timeout"` + RetryMax int `yaml:"retry_max" json:"retry_max"` + RetryWaitMin time.Duration `yaml:"retry_wait_min" json:"retry_wait_min"` + RetryWaitMax time.Duration `yaml:"retry_wait_max" json:"retry_wait_max"` } // DeDiRegistryClient encapsulates the logic for calling the DeDi registry endpoints. @@ -151,6 +152,13 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription return nil, fmt.Errorf("invalid response format: missing details field") } + parentNamespaces := extractStringSlice(data["parent_namespaces"]) + if len(c.config.AllowedParentNamespaces) > 0 { + if len(parentNamespaces) == 0 || !containsAny(parentNamespaces, c.config.AllowedParentNamespaces) { + return nil, fmt.Errorf("registry entry not in allowed parent namespaces") + } + } + // Extract required fields from details signingPublicKey, ok := details["signing_public_key"].(string) if !ok || signingPublicKey == "" { @@ -200,3 +208,46 @@ func parseTime(timeStr string) time.Time { } return parsedTime } + +func extractStringSlice(value interface{}) []string { + if value == nil { + return nil + } + switch v := value.(type) { + case []string: + return v + case []interface{}: + out := make([]string, 0, len(v)) + for _, item := range v { + str, ok := item.(string) + if !ok { + continue + } + if str != "" { + out = append(out, str) + } + } + return out + default: + return nil + } +} + +func containsAny(values []string, allowed []string) bool { + if len(values) == 0 || len(allowed) == 0 { + return false + } + allowedSet := make(map[string]struct{}, len(allowed)) + for _, entry := range allowed { + if entry == "" { + continue + } + allowedSet[entry] = struct{}{} + } + for _, value := range values { + if _, ok := allowedSet[value]; ok { + return true + } + } + return false +} diff --git a/pkg/plugin/implementation/dediregistry/dediregistry_test.go b/pkg/plugin/implementation/dediregistry/dediregistry_test.go index e81ddda..5e85673 100644 --- a/pkg/plugin/implementation/dediregistry/dediregistry_test.go +++ b/pkg/plugin/implementation/dediregistry/dediregistry_test.go @@ -140,6 +140,7 @@ func TestLookup(t *testing.T) { "signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=", "encr_public_key": "test-encr-key", }, + "parent_namespaces": []string{"commerce-network.org", "local-commerce.org"}, "created_at": "2025-10-27T11:45:27.963Z", "updated_at": "2025-10-27T11:46:23.563Z", }, @@ -191,6 +192,94 @@ func TestLookup(t *testing.T) { } }) + t.Run("allowed parent namespaces match", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := map[string]interface{}{ + "message": "Record retrieved from registry cache", + "data": map[string]interface{}{ + "details": map[string]interface{}{ + "url": "http://dev.np2.com/beckn/bap", + "type": "BAP", + "domain": "energy", + "subscriber_id": "dev.np2.com", + "signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=", + }, + "parent_namespaces": []string{"commerce-network.org", "local-commerce.org"}, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + config := &Config{ + URL: server.URL + "/dedi", + RegistryName: "subscribers.beckn.one", + AllowedParentNamespaces: []string{"commerce-network.org"}, + } + + client, closer, err := New(ctx, config) + if err != nil { + t.Fatalf("New() error = %v", err) + } + defer closer() + + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "dev.np2.com", + }, + KeyID: "test-key-id", + } + _, err = client.Lookup(ctx, req) + if err != nil { + t.Errorf("Lookup() error = %v", err) + } + }) + + t.Run("allowed parent namespaces mismatch", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := map[string]interface{}{ + "message": "Record retrieved from registry cache", + "data": map[string]interface{}{ + "details": map[string]interface{}{ + "url": "http://dev.np2.com/beckn/bap", + "type": "BAP", + "domain": "energy", + "subscriber_id": "dev.np2.com", + "signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=", + }, + "parent_namespaces": []string{"local-commerce.org"}, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + config := &Config{ + URL: server.URL + "/dedi", + RegistryName: "subscribers.beckn.one", + AllowedParentNamespaces: []string{"commerce-network.org"}, + } + + client, closer, err := New(ctx, config) + if err != nil { + t.Fatalf("New() error = %v", err) + } + defer closer() + + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "dev.np2.com", + }, + KeyID: "test-key-id", + } + _, err = client.Lookup(ctx, req) + if err == nil { + t.Error("Expected error for disallowed parent namespaces, got nil") + } + }) + // Test empty subscriber ID t.Run("empty subscriber ID", func(t *testing.T) { config := &Config{