Skip to content

fix(ers): coerce attempted_strategies []string to []interface{} for structpb#3645

Draft
damonbanks wants to merge 2 commits into
mainfrom
fix/multi-strategy-attempted-strategies-coerce
Draft

fix(ers): coerce attempted_strategies []string to []interface{} for structpb#3645
damonbanks wants to merge 2 commits into
mainfrom
fix/multi-strategy-attempted-strategies-coerce

Conversation

@damonbanks

Copy link
Copy Markdown

Summary

Service.ResolveEntity in service/entityresolution/multi-strategy/service.go stores attempted_strategies as a raw []string in result.Metadata. The v2 ResolveEntities handler later iterates that metadata into a map[string]interface{} and calls structpb.NewStruct. structpb.NewValue accepts string | float64 | bool | nil | map | []interface{} only — a raw []string is rejected with proto: invalid type: []string, the successful entity resolution is silently dropped via continue in the loop, and the response goes back with empty entityRepresentations.

Downstream effect: any caller that exercises the v2 ResolveEntities path with multi-strategy ERS gets no resolved entities, no entitlements, and (in the case of KAS rewrap) PermissionDenied.

One-line fix: coerce to []interface{} at the point of insertion so all downstream consumers (v1 + v2 ResolveEntities) can serialize it. The existing unit test (service_test.go:300) asserted the metadata type as []string — updated to []interface{} to match the new contract.

Reproduction (before this PR)

The failure is only visible in the full v2 PDP flow (CreateEntityChainsFromTokensResolveEntities on the resolved Entity → flatten AdditionalProps for subject_external_selector_value matching), not in the Entity_Claims direct-pass path the existing examples exercise.

Minimal config (LDAP provider, two-strategy chain so both ERS calls have a matching strategy):

```yaml
services:
entityresolution:
mode: multi-strategy
failure_strategy: fail-fast
providers:
corporate_ldap:
type: ldap
connection: { host: ldap, port: 3890, use_tls: false, bind_dn: "uid=admin,...", bind_password: "..." }
mapping_strategies:
- name: ldap_lookup_from_jwt # call 1: token claims (preferred_username from Okta/Keycloak)
provider: corporate_ldap
entity_type: subject
conditions: { jwt_claims: [ { claim: preferred_username, operator: exists } ] }
input_mapping: [ { jwt_claim: preferred_username, parameter: username, required: true } ]
ldap_search: &search
base_dn: "ou=people,dc=demo,dc=com"
filter: "(&(objectClass=inetOrgPerson)(mail={username}))"
scope: subtree
attributes: [uid, mail, cn, clearance, nationality, needtoknow]
output_mapping: &out
- { source_attribute: uid, claim_name: username }
- { source_attribute: clearance, claim_name: clearance }
- { source_attribute: nationality, claim_name: nationality, transformation: array }
- { source_attribute: needtoknow, claim_name: needtoknow, transformation: array }
- name: ldap_lookup_from_entity # call 2: claims map derived from resolved Entity (userName camelCase)
provider: corporate_ldap
entity_type: subject
conditions: { jwt_claims: [ { claim: userName, operator: exists } ] }
input_mapping: [ { jwt_claim: userName, parameter: username, required: true } ]
ldap_search:
<<: *search
filter: "(&(objectClass=inetOrgPerson)(uid={username}))"
output_mapping: *out
```

Before this PR: authz returns `PermissionDenied`; dsp logs show `failed to create result struct ... error="proto: invalid type: []string"` for the second `ResolveEntities` call.

After this PR: the same call returns the full claim map in `additionalProps`; PDP flattens and matches against `subject_condition_set` selectors; KAS rewraps the key; `tructl decrypt` succeeds.

Why this wasn't caught earlier

The reference multi-strategy ERS runbook validates resolution only via the Web Admin Entity Resolution tool, which posts a JWT wrapped as `Entity_Claims` and renders `additionalProps` for human inspection. The full v2 PDP flow (used by KAS rewrap) is the only path that re-resolves the resolved Entity from call 1; that's what triggers the `[]string`/`structpb` collision in metadata serialization. There is no encrypt→decrypt round-trip in the runbook, so the bug surfaces only when a real authz decision is requested against multi-strategy-resolved attributes.

End-to-end reproduction artifacts

A full Docker Compose stack (Okta + LLDAP + DSP built against a patched service module) demonstrating both the failure (without this PR) and the fix (with this PR) is at:

virtru-corp/integration-studios#80 → `okta-multi-strategy-ldap/` — see `BUG_REPORT.md` and `README.md` → Known platform limitation.

The integration repo includes:

  • A `test_ers.py` that exercises both v2 endpoints directly and shows the before/after `additionalProps` shape
  • A `demo.py` that runs Okta auth → encrypt → decrypt end-to-end
  • An `ldap_source_marker` canary attribute (an LDAP custom attr the JWT cannot possibly carry) — used to verify the LDAP-resolved data really survives to the PDP

Adjacent issue (not in this PR)

`service/entityresolution/multi-strategy/output_mapper.go` → `applyTransformation`: the switch supports `array | csv_to_array | ldap_dn_to_cn_array | lowercase | uppercase | trim`. The constant `LDAPAttrValues = "ldap_attribute_values"` (referenced in `transformation/constants_test.go` and named in the multi-strategy README and downstream runbooks) is not wired into the switch — using it errors at runtime with `unknown transformation: ldap_attribute_values`. Easy follow-up: either implement the case or remove from the docs. Happy to open a separate PR.

Testing

  • `go test ./service/entityresolution/multi-strategy/...` — green (the one existing test that depended on the old type assertion is updated to assert `[]interface{}`)
  • End-to-end TDF encrypt → decrypt verified locally against a DSP build that vendors this patch, with a Connect-RPC LDAP provider returning multi-valued ABAC attributes and an LDAP-only canary claim that survives end-to-end

Checklist

  • Unit tests pass (existing test updated to assert the new contract; behaviour change is purely the metadata value's Go type)
  • Integration tests — none added in this PR (the existing multistrategy integration suite asserts entity types/counts via `CreateEntityChainsFromTokens`, which is not affected by this bug). Happy to add a focused regression test if maintainers want.
  • No public-API change in the proto / connect surface (only the Go type stored in an opaque metadata map)

…tructpb

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.
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 898ffb63-6962-48a8-a24f-7a81e22ef720

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/multi-strategy-attempted-strategies-coerce

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a serialization compatibility issue within the multi-strategy entity resolution service. By coercing the 'attempted_strategies' metadata field to '[]interface{}', the service now correctly serializes metadata into 'structpb', preventing silent failures and downstream authorization errors during the v2 'ResolveEntities' flow.

Highlights

  • Type Coercion for Metadata: Coerced the 'attempted_strategies' slice from []string to []interface{} in 'Service.ResolveEntity' to ensure compatibility with 'structpb.NewValue'.
  • Bug Fix: Resolved an issue where '[]string' types caused 'structpb' serialization failures, leading to silently dropped entities and 'PermissionDenied' errors in the v2 'ResolveEntities' flow.
  • Test Update: Updated the existing unit test in 'service_test.go' to assert the new '[]interface{}' contract for metadata.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


The slice was string, a rigid type, But structpb needed more to be ripe. With interface cast, the data flows, And now the authz correctly shows.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the multi-strategy entity resolution service to coerce the 'attempted_strategies' metadata from a slice of strings to a slice of empty interfaces. This ensures compatibility with structpb.NewValue serialization and prevents resolved entities from being silently dropped. The corresponding unit tests were updated to match this type change. Feedback was provided to rename the loop variable 's' to avoid shadowing the method receiver of the same name.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread service/entityresolution/multi-strategy/service.go Outdated
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 176.442917ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 98.579874ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 438.443798ms
Throughput 228.08 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 43.842505541s
Average Latency 436.594724ms
Throughput 114.04 requests/second

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 195.583594ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 102.304404ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 433.470072ms
Throughput 230.70 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 45.354547925s
Average Latency 451.637846ms
Throughput 110.24 requests/second

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Govulncheck found vulnerabilities ⚠️

The following modules have known vulnerabilities:

  • examples
  • otdfctl
  • sdk
  • service
  • lib/fixtures
  • tests-bdd

See the workflow run for details.

jp-ayyappan added a commit that referenced this pull request Jun 19, 2026
…tructpb

Storing attempted_strategies as []string in result.Metadata caused
structpb.NewValue to reject the value (proto: invalid type: []string)
in the v2 ResolveEntities handler. The entity was silently dropped via
continue, returning empty entityRepresentations. For any caller using the
chain-entity path (e.g. JustInTimePDP during KAS rewrap), this produced
PermissionDenied.

Coerce to []interface{} at the point of insertion so all downstream
consumers can serialize it.

Backport of #3645 onto service/v0.15.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant