From ddd3db6416e22e1551353f6fc53c89a496fb3900 Mon Sep 17 00:00:00 2001 From: Damon Banks Date: Thu, 18 Jun 2026 13:24:59 -0600 Subject: [PATCH 1/2] fix(ers): coerce attempted_strategies []string to []interface{} for structpb The v2 ResolveEntities handler iterates result.Metadata into a map[string]interface{} and calls structpb.NewStruct on it. structpb's NewValue accepts string|float64|bool|nil|map|[]interface{} only - a raw []string trips 'proto: invalid type: []string' and the successful entity resolution is silently dropped via 'continue' in the loop. Downstream authz sees an empty entityRepresentations response, has nothing to evaluate against subject_condition_sets, and denies the request. This blocks any TDF rewrap that depends on multi-strategy ERS to supply ABAC attributes (the failure mode requires the two-call PDP flow: CreateEntityChainsFromTokens then ResolveEntities on the resolved Entity). The first call works because identifier-only Entities don't trigger struct serialization; the second call serializes Metadata and hits the type rejection. Coerce attemptedStrategies to []interface{} at the point of insertion so all downstream consumers (v1 + v2 ResolveEntities) can serialize it. Existing unit test updated to assert the new contract. See virtru-corp/integration-studios#80 for the full upstream-facing reproduction. --- service/entityresolution/multi-strategy/service.go | 12 +++++++++++- .../entityresolution/multi-strategy/service_test.go | 8 ++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/service/entityresolution/multi-strategy/service.go b/service/entityresolution/multi-strategy/service.go index 1a37f32154..09bfe8896e 100644 --- a/service/entityresolution/multi-strategy/service.go +++ b/service/entityresolution/multi-strategy/service.go @@ -116,7 +116,17 @@ func (s *Service) ResolveEntity(ctx context.Context, entityID string, claimsMap result.Metadata["strategy_provider"] = strategy.Provider result.Metadata["entity_type"] = strategy.EntityType result.Metadata["failure_strategy"] = failureStrategy - result.Metadata["attempted_strategies"] = attemptedStrategies + // Coerce []string -> []interface{} so structpb.NewValue (called by + // the v2 ResolveEntities handler when it serializes metadata into + // EntityRepresentation.AdditionalProps) can encode it. structpb's + // NewValue accepts string|float64|bool|nil|map|[]interface{} only - + // a raw []string trips "proto: invalid type: []string" and the + // resolved entity is silently dropped via `continue` in the loop. + attemptedAny := make([]interface{}, len(attemptedStrategies)) + for i, s := range attemptedStrategies { + attemptedAny[i] = s + } + result.Metadata["attempted_strategies"] = attemptedAny return result, nil } diff --git a/service/entityresolution/multi-strategy/service_test.go b/service/entityresolution/multi-strategy/service_test.go index d767db1bc2..130bb7da0b 100644 --- a/service/entityresolution/multi-strategy/service_test.go +++ b/service/entityresolution/multi-strategy/service_test.go @@ -296,8 +296,12 @@ func TestMultiStrategyService_FailureStrategyContinue(t *testing.T) { t.Errorf("Expected entity_type '%s', got '%v'", types.EntityTypeSubject, result.Metadata["entity_type"]) } - // Verify attempted strategies metadata - attemptedStrategies, ok := result.Metadata["attempted_strategies"].([]string) + // Verify attempted strategies metadata. Stored as []interface{} (rather + // than []string) so the value can flow through structpb.NewValue when + // the v2 ResolveEntities handler serializes result.Metadata into + // EntityRepresentation.AdditionalProps; structpb does not accept + // []string. + attemptedStrategies, ok := result.Metadata["attempted_strategies"].([]interface{}) if !ok || len(attemptedStrategies) != 2 { t.Errorf("Expected attempted_strategies to contain 2 strategies, got %v", result.Metadata["attempted_strategies"]) } From a446663ff4e88c0b1e0ef73ea1fe64e83d419a82 Mon Sep 17 00:00:00 2001 From: Damon <149414575+damonbanks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:22:39 -0600 Subject: [PATCH 2/2] Update service/entityresolution/multi-strategy/service.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- service/entityresolution/multi-strategy/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/entityresolution/multi-strategy/service.go b/service/entityresolution/multi-strategy/service.go index 09bfe8896e..0cd91048f2 100644 --- a/service/entityresolution/multi-strategy/service.go +++ b/service/entityresolution/multi-strategy/service.go @@ -123,8 +123,8 @@ func (s *Service) ResolveEntity(ctx context.Context, entityID string, claimsMap // a raw []string trips "proto: invalid type: []string" and the // resolved entity is silently dropped via `continue` in the loop. attemptedAny := make([]interface{}, len(attemptedStrategies)) - for i, s := range attemptedStrategies { - attemptedAny[i] = s + for i, strat := range attemptedStrategies { + attemptedAny[i] = strat } result.Metadata["attempted_strategies"] = attemptedAny