feat: add allowed parent namespaces for signature validation
This commit is contained in:
@@ -18,6 +18,7 @@ registry:
|
|||||||
config:
|
config:
|
||||||
url: "https://dedi-wrapper.example.com/dedi"
|
url: "https://dedi-wrapper.example.com/dedi"
|
||||||
registryName: "subscribers.beckn.one"
|
registryName: "subscribers.beckn.one"
|
||||||
|
allowedParentNamespaces: "commerce-network.org,retail-collective.org"
|
||||||
timeout: 30
|
timeout: 30
|
||||||
retry_max: 3
|
retry_max: 3
|
||||||
retry_wait_min: 1s
|
retry_wait_min: 1s
|
||||||
@@ -30,6 +31,7 @@ registry:
|
|||||||
|-----------|----------|-------------|---------|
|
|-----------|----------|-------------|---------|
|
||||||
| `url` | Yes | DeDi wrapper API base URL (include /dedi path) | - |
|
| `url` | Yes | DeDi wrapper API base URL (include /dedi path) | - |
|
||||||
| `registryName` | Yes | Registry name for lookup 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 |
|
| `timeout` | No | Request timeout in seconds | Client default |
|
||||||
| `retry_max` | No | Maximum number of retry attempts | 4 (library 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) |
|
| `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=",
|
"signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=",
|
||||||
"encr_public_key": "test-encr-key"
|
"encr_public_key": "test-encr-key"
|
||||||
},
|
},
|
||||||
|
"parent_namespaces": ["commerce-network.org", "local-commerce.org"],
|
||||||
"created_at": "2025-10-27T11:45:27.963Z",
|
"created_at": "2025-10-27T11:45:27.963Z",
|
||||||
"updated_at": "2025-10-27T11:46:23.563Z"
|
"updated_at": "2025-10-27T11:46:23.563Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/beckn-one/beckn-onix/pkg/log"
|
"github.com/beckn-one/beckn-onix/pkg/log"
|
||||||
"github.com/beckn-one/beckn-onix/pkg/plugin/definition"
|
"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)
|
log.Debugf(ctx, "DeDi Registry config mapped: %+v", dediConfig)
|
||||||
|
|
||||||
dediClient, closer, err := dediregistry.New(ctx, 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
|
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
|
// Provider is the exported plugin instance
|
||||||
var Provider = dediRegistryProvider{}
|
var Provider = dediRegistryProvider{}
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ import (
|
|||||||
|
|
||||||
// Config holds configuration parameters for the DeDi registry client.
|
// Config holds configuration parameters for the DeDi registry client.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
URL string `yaml:"url" json:"url"`
|
URL string `yaml:"url" json:"url"`
|
||||||
RegistryName string `yaml:"registryName" json:"registryName"`
|
RegistryName string `yaml:"registryName" json:"registryName"`
|
||||||
Timeout int `yaml:"timeout" json:"timeout"`
|
AllowedParentNamespaces []string `yaml:"allowedParentNamespaces" json:"allowedParentNamespaces"`
|
||||||
RetryMax int `yaml:"retry_max" json:"retry_max"`
|
Timeout int `yaml:"timeout" json:"timeout"`
|
||||||
RetryWaitMin time.Duration `yaml:"retry_wait_min" json:"retry_wait_min"`
|
RetryMax int `yaml:"retry_max" json:"retry_max"`
|
||||||
RetryWaitMax time.Duration `yaml:"retry_wait_max" json:"retry_wait_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.
|
// 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")
|
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
|
// Extract required fields from details
|
||||||
signingPublicKey, ok := details["signing_public_key"].(string)
|
signingPublicKey, ok := details["signing_public_key"].(string)
|
||||||
if !ok || signingPublicKey == "" {
|
if !ok || signingPublicKey == "" {
|
||||||
@@ -200,3 +208,46 @@ func parseTime(timeStr string) time.Time {
|
|||||||
}
|
}
|
||||||
return parsedTime
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ func TestLookup(t *testing.T) {
|
|||||||
"signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=",
|
"signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=",
|
||||||
"encr_public_key": "test-encr-key",
|
"encr_public_key": "test-encr-key",
|
||||||
},
|
},
|
||||||
|
"parent_namespaces": []string{"commerce-network.org", "local-commerce.org"},
|
||||||
"created_at": "2025-10-27T11:45:27.963Z",
|
"created_at": "2025-10-27T11:45:27.963Z",
|
||||||
"updated_at": "2025-10-27T11:46:23.563Z",
|
"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
|
// Test empty subscriber ID
|
||||||
t.Run("empty subscriber ID", func(t *testing.T) {
|
t.Run("empty subscriber ID", func(t *testing.T) {
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
|||||||
Reference in New Issue
Block a user