feat: added network ID allowlist in DeDi registry plugin
This commit is contained in:
@@ -18,7 +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"
|
allowedNetworkIDs: "commerce-network/subscriber-references,local-commerce/subscriber-references"
|
||||||
timeout: 30
|
timeout: 30
|
||||||
retry_max: 3
|
retry_max: 3
|
||||||
retry_wait_min: 1s
|
retry_wait_min: 1s
|
||||||
@@ -31,7 +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 | - |
|
| `allowedNetworkIDs` | No | Allowlist of network membership IDs from `data.network_memberships` 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) |
|
||||||
@@ -39,15 +39,15 @@ registry:
|
|||||||
|
|
||||||
## API Integration
|
## API Integration
|
||||||
|
|
||||||
### DeDi Wrapper API Format
|
### Beckn Registry API Format
|
||||||
```
|
```
|
||||||
GET {url}/lookup/{subscriber_id}/{registryName}/{key_id}
|
GET {url}/lookup/{subscriber_id}/{registryName}/{key_id}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example**: `https://dedi-wrapper.com/dedi/lookup/bpp.example.com/subscribers.beckn.one/key-1`
|
**Example**: `https://api.bekcn.io/registry/dedi/lookup/bpp.example.com/subscribers.beckn.one/76EU7K8oC9EQbXPMRL5uw3KbmTxbg3YDXHvm9nVQpK2eGghASnwHzm`
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
**No authentication required** - DeDi wrapper API is public.
|
**No authentication required** - Beckn Registry API is public.
|
||||||
|
|
||||||
### Expected Response Format
|
### Expected Response Format
|
||||||
|
|
||||||
@@ -64,7 +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"],
|
"network_memberships": ["commerce-network/subscriber-references", "local-commerce/subscriber-references"],
|
||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -94,8 +94,9 @@ modules:
|
|||||||
registry:
|
registry:
|
||||||
id: dediregistry
|
id: dediregistry
|
||||||
config:
|
config:
|
||||||
url: "https://dedi-wrapper.example.com/dedi"
|
url: "https://api.bekcn.io/registry/dedi"
|
||||||
registryName: "subscribers.beckn.one"
|
registryName: "subscribers.beckn.one"
|
||||||
|
allowedNetworkIDs: "commerce-network/subscriber-references,local-commerce/subscriber-references"
|
||||||
timeout: 30
|
timeout: 30
|
||||||
retry_max: 3
|
retry_max: 3
|
||||||
retry_wait_min: 1s
|
retry_wait_min: 1s
|
||||||
@@ -148,6 +149,7 @@ This plugin replaces direct DeDi API integration with the new DeDi Wrapper API f
|
|||||||
- **Added**: Configurable registryName parameter
|
- **Added**: Configurable registryName parameter
|
||||||
- **Changed**: POST requests → GET requests
|
- **Changed**: POST requests → GET requests
|
||||||
- **Updated**: Response structure parsing (`data.details` object)
|
- **Updated**: Response structure parsing (`data.details` object)
|
||||||
|
- **Updated**: Optional allowlist validation now checks `data.network_memberships`
|
||||||
- **Added**: New URL path parameter format
|
- **Added**: New URL path parameter format
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ func (d dediRegistryProvider) New(ctx context.Context, config map[string]string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rawNamespaces, exists := config["allowedParentNamespaces"]; exists && rawNamespaces != "" {
|
if rawNetworkIDs, exists := config["allowedNetworkIDs"]; exists && rawNetworkIDs != "" {
|
||||||
dediConfig.AllowedParentNamespaces = parseAllowedParentNamespaces(rawNamespaces)
|
dediConfig.AllowedNetworkIDs = parseAllowedNetworkIDs(rawNetworkIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf(ctx, "DeDi Registry config mapped: %+v", dediConfig)
|
log.Debugf(ctx, "DeDi Registry config mapped: %+v", dediConfig)
|
||||||
@@ -51,17 +51,17 @@ func (d dediRegistryProvider) New(ctx context.Context, config map[string]string)
|
|||||||
return dediClient, closer, nil
|
return dediClient, closer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAllowedParentNamespaces(raw string) []string {
|
func parseAllowedNetworkIDs(raw string) []string {
|
||||||
parts := strings.Split(raw, ",")
|
parts := strings.Split(raw, ",")
|
||||||
namespaces := make([]string, 0, len(parts))
|
networkIDs := make([]string, 0, len(parts))
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
item := strings.TrimSpace(part)
|
item := strings.TrimSpace(part)
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
namespaces = append(namespaces, item)
|
networkIDs = append(networkIDs, item)
|
||||||
}
|
}
|
||||||
return namespaces
|
return networkIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider is the exported plugin instance
|
// Provider is the exported plugin instance
|
||||||
|
|||||||
@@ -90,6 +90,23 @@ func TestDediRegistryProvider_New_InvalidTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseAllowedNetworkIDs(t *testing.T) {
|
||||||
|
got := parseAllowedNetworkIDs("commerce-network/subscriber-references, retail-network/subscriber-references, ,")
|
||||||
|
want := []string{
|
||||||
|
"commerce-network/subscriber-references",
|
||||||
|
"retail-network/subscriber-references",
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("expected %d allowed network IDs, got %d", len(want), len(got))
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("expected allowedNetworkIDs[%d] = %q, got %q", i, want[i], got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDediRegistryProvider_New_NilContext(t *testing.T) {
|
func TestDediRegistryProvider_New_NilContext(t *testing.T) {
|
||||||
provider := dediRegistryProvider{}
|
provider := dediRegistryProvider{}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +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"`
|
||||||
AllowedParentNamespaces []string `yaml:"allowedParentNamespaces" json:"allowedParentNamespaces"`
|
AllowedNetworkIDs []string `yaml:"allowedNetworkIDs" json:"allowedNetworkIDs"`
|
||||||
Timeout int `yaml:"timeout" json:"timeout"`
|
Timeout int `yaml:"timeout" json:"timeout"`
|
||||||
RetryMax int `yaml:"retry_max" json:"retry_max"`
|
RetryMax int `yaml:"retry_max" json:"retry_max"`
|
||||||
RetryWaitMin time.Duration `yaml:"retry_wait_min" json:"retry_wait_min"`
|
RetryWaitMin time.Duration `yaml:"retry_wait_min" json:"retry_wait_min"`
|
||||||
RetryWaitMax time.Duration `yaml:"retry_wait_max" json:"retry_wait_max"`
|
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.
|
||||||
@@ -164,11 +164,11 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription
|
|||||||
detailsDomain, _ := details["domain"].(string)
|
detailsDomain, _ := details["domain"].(string)
|
||||||
detailsSubscriberID, _ := details["subscriber_id"].(string)
|
detailsSubscriberID, _ := details["subscriber_id"].(string)
|
||||||
|
|
||||||
// Validate parent namespaces if configured
|
// Validate network memberships if configured.
|
||||||
parentNamespaces := extractStringSlice(data["parent_namespaces"])
|
networkMemberships := extractStringSlice(data["network_memberships"])
|
||||||
if len(c.config.AllowedParentNamespaces) > 0 {
|
if len(c.config.AllowedNetworkIDs) > 0 {
|
||||||
if len(parentNamespaces) == 0 || !containsAny(parentNamespaces, c.config.AllowedParentNamespaces) {
|
if len(networkMemberships) == 0 || !containsAny(networkMemberships, c.config.AllowedNetworkIDs) {
|
||||||
return nil, fmt.Errorf("registry entry with subscriber_id '%s' does not belong to any configured parent namespaces (registry.config.allowedParentNamespaces)", detailsSubscriberID)
|
return nil, fmt.Errorf("registry entry with subscriber_id '%s' does not belong to any configured network memberships (registry.config.allowedNetworkIDs)", detailsSubscriberID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,9 +140,9 @@ 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"},
|
"network_memberships": []string{"commerce-network/subscriber-references", "local-commerce/subscriber-references"},
|
||||||
"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",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -192,7 +192,7 @@ func TestLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("allowed parent namespaces match", func(t *testing.T) {
|
t.Run("allowed network IDs match", func(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"message": "Record retrieved from registry cache",
|
"message": "Record retrieved from registry cache",
|
||||||
@@ -204,7 +204,7 @@ func TestLookup(t *testing.T) {
|
|||||||
"subscriber_id": "dev.np2.com",
|
"subscriber_id": "dev.np2.com",
|
||||||
"signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=",
|
"signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=",
|
||||||
},
|
},
|
||||||
"parent_namespaces": []string{"commerce-network.org", "local-commerce.org"},
|
"network_memberships": []string{"commerce-network/subscriber-references", "local-commerce/subscriber-references"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -213,9 +213,9 @@ func TestLookup(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
URL: server.URL + "/dedi",
|
URL: server.URL + "/dedi",
|
||||||
RegistryName: "subscribers.beckn.one",
|
RegistryName: "subscribers.beckn.one",
|
||||||
AllowedParentNamespaces: []string{"commerce-network.org"},
|
AllowedNetworkIDs: []string{"commerce-network/subscriber-references"},
|
||||||
}
|
}
|
||||||
|
|
||||||
client, closer, err := New(ctx, config)
|
client, closer, err := New(ctx, config)
|
||||||
@@ -236,7 +236,7 @@ func TestLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("allowed parent namespaces mismatch", func(t *testing.T) {
|
t.Run("allowed network IDs mismatch", func(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"message": "Record retrieved from registry cache",
|
"message": "Record retrieved from registry cache",
|
||||||
@@ -248,7 +248,7 @@ func TestLookup(t *testing.T) {
|
|||||||
"subscriber_id": "dev.np2.com",
|
"subscriber_id": "dev.np2.com",
|
||||||
"signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=",
|
"signing_public_key": "384qqkIIpxo71WaJPsWqQNWUDGAFnfnJPxuDmtuBiLo=",
|
||||||
},
|
},
|
||||||
"parent_namespaces": []string{"local-commerce.org"},
|
"network_memberships": []string{"local-commerce/subscriber-references"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -257,9 +257,9 @@ func TestLookup(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
URL: server.URL + "/dedi",
|
URL: server.URL + "/dedi",
|
||||||
RegistryName: "subscribers.beckn.one",
|
RegistryName: "subscribers.beckn.one",
|
||||||
AllowedParentNamespaces: []string{"commerce-network.org"},
|
AllowedNetworkIDs: []string{"commerce-network/subscriber-references"},
|
||||||
}
|
}
|
||||||
|
|
||||||
client, closer, err := New(ctx, config)
|
client, closer, err := New(ctx, config)
|
||||||
@@ -276,7 +276,11 @@ func TestLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
_, err = client.Lookup(ctx, req)
|
_, err = client.Lookup(ctx, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected error for disallowed parent namespaces, got nil")
|
t.Error("Expected error for disallowed network memberships, got nil")
|
||||||
|
}
|
||||||
|
expectedErr := "registry entry with subscriber_id 'dev.np2.com' does not belong to any configured network memberships (registry.config.allowedNetworkIDs)"
|
||||||
|
if err.Error() != expectedErr {
|
||||||
|
t.Errorf("Expected error %q, got %q", expectedErr, err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user