Copilot commented on code in PR #2807:
URL: 
https://github.com/apache/apisix-ingress-controller/pull/2807#discussion_r3602359455


##########
internal/webhook/v1/consumer_webhook.go:
##########
@@ -227,15 +228,101 @@ func (v *ConsumerCustomValidator) 
extractCredentialKey(ctx context.Context, cons
                return "", nil
        }
 
-       var cfg struct {
-               Key string `json:"key"`
+       key, err := parseInlineKeyAuthKey(credential.Config.Raw)
+       if err != nil {
+               return "", fmt.Errorf("invalid key-auth credential config for 
Consumer %s/%s: %w",
+                       consumer.Namespace, consumer.Name, err)
        }
-       if err := json.Unmarshal(credential.Config.Raw, &cfg); err != nil {
-               // Malformed JSON is not a hard error: skip duplicate detection 
for this
-               // credential so existing consumers with bad config are not 
suddenly denied.
-               consumerLog.V(1).Info("skipping duplicate key-auth check: 
malformed credential config",
-                       "consumer", consumer.Name, "error", err)
+       return key, nil
+}
+
+// parseInlineKeyAuthKey extracts the key-auth "key" from an inline credential
+// config the same way downstream cjson does: exact-case, string-valued,
+// last-wins. Ambiguous configs that Go's struct decoder would silently reject
+// while cjson still resolves to a live key (duplicate "key" members, or a
+// non-string "key") are returned as errors so they can't bypass the duplicate
+// check. Genuinely malformed JSON that cjson also rejects returns ("", nil) so
+// existing consumers with broken config are skipped, not suddenly denied.
+func parseInlineKeyAuthKey(raw []byte) (string, error) {
+       dec := json.NewDecoder(bytes.NewReader(raw))
+
+       // Top level must be an object, else there is no usable key.
+       tok, err := dec.Token()
+       if err != nil {
                return "", nil
        }
-       return cfg.Key, nil
+       if delim, ok := tok.(json.Delim); !ok || delim != '{' {
+               return "", nil
+       }
+
+       var (
+               key      string
+               keyCount int
+       )
+       for dec.More() {
+               nameTok, err := dec.Token()
+               if err != nil {
+                       return "", nil
+               }
+               name, ok := nameTok.(string)
+               if !ok {
+                       return "", nil
+               }
+               if name != "key" {
+                       if err := skipJSONValue(dec); err != nil {
+                               return "", nil
+                       }
+                       continue
+               }
+
+               keyCount++
+               valTok, err := dec.Token()
+               if err != nil {
+                       return "", nil
+               }
+               switch val := valTok.(type) {
+               case string:
+                       key = val
+               case nil:
+                       // null key: no usable value, but still counts for dup 
detection.
+               default:
+                       // number/bool/object/array: cjson would deliver a 
value here while
+                       // Go's struct decoder errors and skips. Reject instead.
+                       return "", fmt.Errorf("key-auth credential \"key\" must 
be a string")
+               }
+       }
+
+       if keyCount > 1 {
+               return "", fmt.Errorf("key-auth credential config has duplicate 
\"key\" members")
+       }
+       return key, nil

Review Comment:
   parseInlineKeyAuthKey can treat malformed JSON as valid because it never 
consumes the closing '}' or checks for trailing non-whitespace bytes. 
json.Decoder permits streaming multiple top-level values, so inputs like 
`{"key":"K"` (truncated) or `{"key":"K"}{"key":"X"}` would currently return a 
key instead of being treated as malformed and skipped, contradicting the 
documented leniency for malformed configs.



##########
internal/webhook/v1/consumer_webhook_test.go:
##########
@@ -192,3 +192,67 @@ func 
TestConsumerValidator_DenyDuplicateKeyAuthCredential(t *testing.T) {
        require.Contains(t, err.Error(), `duplicate key-auth credential key 
"shared-key"`)
        require.Contains(t, err.Error(), "default/existing")
 }
+
+// A duplicate-key inline config ({"key":123,"key":"K"}) is unreadable to Go's
+// struct decoder but resolves to "K" downstream via cjson. The webhook must
+// reject it instead of silently skipping the duplicate check.
+func TestConsumerValidator_DenyDuplicateKeyAuthCredential_ParserDivergence(t 
*testing.T) {
+       existing := &apisixv1alpha1.Consumer{
+               ObjectMeta: metav1.ObjectMeta{Name: "existing", Namespace: 
"default"},
+               Spec: apisixv1alpha1.ConsumerSpec{
+                       GatewayRef: apisixv1alpha1.GatewayRef{Name: 
"test-gateway"},
+                       Credentials: []apisixv1alpha1.Credential{{
+                               Type:   "key-auth",
+                               Config: apiextensionsv1.JSON{Raw: 
[]byte(`{"key":"victims-key"}`)},
+                       }},
+               },
+       }
+       consumer := &apisixv1alpha1.Consumer{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: apisixv1alpha1.ConsumerSpec{
+                       GatewayRef: apisixv1alpha1.GatewayRef{Name: 
"test-gateway"},
+                       Credentials: []apisixv1alpha1.Credential{{
+                               Type:   "key-auth",
+                               Config: apiextensionsv1.JSON{Raw: 
[]byte(`{"key":123,"key":"victims-key"}`)},
+                       }},
+               },
+       }
+
+       validator := buildConsumerValidator(t, existing)
+
+       _, err := validator.ValidateCreate(context.Background(), consumer)
+       require.Error(t, err)
+       require.Contains(t, err.Error(), "invalid key-auth credential config")
+}
+
+func TestParseInlineKeyAuthKey(t *testing.T) {
+       tests := []struct {
+               name    string
+               raw     string
+               wantKey string
+               wantErr bool
+       }{
+               {name: "plain string key", raw: `{"key":"K"}`, wantKey: "K"},
+               {name: "extra fields ignored", raw: 
`{"key":"K","foo":{"a":1}}`, wantKey: "K"},
+               {name: "duplicate key members", raw: `{"key":"a","key":"b"}`, 
wantErr: true},
+               {name: "number then string (divergence PoC)", raw: 
`{"key":123,"key":"K"}`, wantErr: true},
+               {name: "non-string key", raw: `{"key":123}`, wantErr: true},
+               {name: "object key", raw: `{"key":{"nested":1}}`, wantErr: 
true},
+               {name: "null key skipped", raw: `{"key":null}`, wantKey: ""},
+               {name: "no key member", raw: `{"foo":"bar"}`, wantKey: ""},
+               {name: "exact-case only, Key ignored", raw: `{"Key":"K"}`, 
wantKey: ""},
+               {name: "malformed json skipped", raw: `{"key":`, wantKey: ""},
+               {name: "non-object skipped", raw: `["key","K"]`, wantKey: ""},
+       }

Review Comment:
   The parser tests cover a malformed prefix (`{"key":`) but not other 
malformed shapes that the current token-walk can mistakenly accept (e.g., 
truncated objects or multiple top-level JSON values). Adding these cases would 
prevent regressions around the intended "malformed JSON is skipped" behavior.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to