Skip to content

fix(ofrep): new SSE event stream endpoint and conditional request support#5957

Merged
erka merged 2 commits into
v2from
rd/v2/ofrep-schema-change
Jun 3, 2026
Merged

fix(ofrep): new SSE event stream endpoint and conditional request support#5957
erka merged 2 commits into
v2from
rd/v2/ofrep-schema-change

Conversation

@erka

@erka erka commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
  • The origin field was marked as optional in grpc and it was always returned as an empty string, which breaks the ofrep-web JS client logic for constructing the streaming URL.
  • The snapshotDigest was removed to align with current implementation details in the ofrep-web JS client. Removing it causes an extra re-evaluation fetch to the bulk endpoint on each (re)connect. Ideally, it should be avoided but I don't see a good solution at the moment.
  • Include Etag in CORS exposed headers for cross-origin reads. The Access-Control-Expose-Headers response header only listed Link, preventing browser JS from reading the Etag header cross-origin. This broke conditional request caching via If-None-Match. Added Etag to the exposed headers list.
  • Move SSE stream endpoint to /ofrep/v1/_stream/{env}/{ns}/events to support skip authentication for in-process OFREP stream requests

related #5669 61aa322

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

The latest Buf updates on your PR. Results from workflow Proto / proto-lint (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed❌ failed (1)Jun 3, 2026, 1:02 PM

@github-actions github-actions 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.

Verdict: comment

Clean PR fixing OFREP cross-origin caching and removing unused response fields. Add reserved for the deleted proto field to stay consistent with project conventions.

rpc/flipt/ofrep/ofrep.proto

  • minor (L40): Reserve the deleted origin field (reserved 2; reserved "origin";) before the closing brace to follow the project's protobuf conventions (e.g., rpc/v2/evaluation/evaluationv2.proto) and prevent accidental reuse of the field number.

🤖 Automated review by the Flipt PR review agent.

@erka erka added the v2 Flipt v2 label Jun 1, 2026
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.41558% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.41%. Comparing base (c7d6fb3) to head (08903c7).

Files with missing lines Patch % Lines
internal/gateway/gateway.go 36.36% 4 Missing and 3 partials ⚠️
internal/server/evaluation/client/server.go 90.00% 1 Missing and 1 partial ⚠️
internal/server/middleware/grpc/middleware.go 50.00% 1 Missing and 1 partial ⚠️
internal/cmd/http.go 96.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               v2    #5957      +/-   ##
==========================================
+ Coverage   61.29%   61.41%   +0.12%     
==========================================
  Files         141      142       +1     
  Lines       14211    14265      +54     
==========================================
+ Hits         8710     8761      +51     
+ Misses       4772     4770       -2     
- Partials      729      734       +5     
Flag Coverage Δ
integrationtests 34.51% <42.85%> (-0.01%) ⬇️
unittests 52.48% <72.72%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@erka
erka marked this pull request as ready for review June 1, 2026 12:41
@erka
erka requested a review from a team as a code owner June 1, 2026 12:41
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 1, 2026
string request_uri = 1 [(google.api.field_behavior) = REQUIRED];
// The scheme + host + optional port portion of the endpoint URL.
string origin = 2 [(google.api.field_behavior) = OPTIONAL];
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we should add this field to reserved like the review says so we dont accidentally reuse it in the future

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually, I am blind... I've missed optional in definition which causes the issue. Thanks for pointing on this.

@erka
erka force-pushed the rd/v2/ofrep-schema-change branch from 9461496 to 88d0a4f Compare June 1, 2026 14:08

@github-actions github-actions 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.

Verdict: comment

Clean PR fixing OFREP cross-origin conditional request support by exposing Etag and allowing If-None-Match through CORS, while correcting the bulk evaluation response format. One clarification needed: the origin proto field was made explicitly optional, not deleted, so adding reserved for it would conflict with the still-active field and break protobuf compilation.

rpc/flipt/ofrep/ofrep.proto

  • minor (L40): The origin field was changed from string to optional string, not removed. Field number 2 is still actively used by origin, so adding reserved 2; or reserved "origin"; would cause a protobuf compilation error. No reservation is needed—this is a wire-compatible presence-tracking change that keeps the field in place.

🤖 Automated review by the Flipt PR review agent.

- The origin field was marked as optional in grpc and it was always returned
as an empty string, which breaks the ofrep-web JS client logic for
constructing the streaming URL.
- The snapshotDigest was removed to align with current implementation
details in the ofrep-web JS client. Removing it causes an extra
re-evaluation fetch to the bulk endpoint on each (re)connect. Ideally,
it should be avoided but I don't see a good solution at the moment.
- Include Etag in CORS exposed headers for cross-origin reads. The
Access-Control-Expose-Headers response header only listed Link, preventing
browser JS from reading the Etag header cross-origin. This broke
conditional request caching via If-None-Match. Added Etag to the exposed
headers list.

related #5669 61aa322

Signed-off-by: Roman Dmytrenko <[email protected]>
@erka
erka force-pushed the rd/v2/ofrep-schema-change branch from 88d0a4f to 85a0cf4 Compare June 1, 2026 17:59

@github-actions github-actions 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.

Verdict: approve

Clean, focused PR. The CORS changes correctly expose ETag and allow If-None-Match for cross-origin conditional requests, the proto origin field is appropriately made optional for presence tracking, and the snapshotDigest removal aligns the response with current client expectations. Tests and schemas are consistently updated. No issues found.

🤖 Automated review by the Flipt PR review agent.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jun 2, 2026

@github-actions github-actions 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.

Verdict: request changes

There's a critical authentication bypass: when authentication.exclude.ofrep is enabled, any external HTTP client can skip authentication for all client evaluation endpoints by sending the X-Flipt-OFREP-Stream header. The inproc peer check doesn't distinguish internal alias requests from direct external requests because all HTTP traffic goes through the in-process gRPC channel. Use a context-bound signal that external clients cannot spoof instead of forwardable HTTP metadata.

internal/server/evaluation/client/server.go

  • critical (L197): Authentication bypass for all client evaluation endpoints when authentication.exclude.ofrep is enabled. SkipsAuthentication returns true when X-Flipt-OFREP-Stream metadata equals "true" and the peer network is inproc. Both conditions are trivially met by any external HTTP client:
  1. ForwardFliptOFREPStream (in internal/server/middleware/grpc/middleware.go) forwards this header by reading the raw *http.Request header map, bypassing the isReservedHeader block in the gateway's incoming header matcher.
  2. Every HTTP request goes through the in-process gRPC channel (inprocgrpc), so the peer is always inproc regardless of whether the request came from the internal ofrepStreamHandler alias or a direct external call to /client/v2/....

An attacker can therefore evaluate flags or stream snapshots without authentication by sending X-Flipt-OFREP-Stream: true to any client evaluation path.

Fix: Stop relying on a client-controllable HTTP header. Have ofrepStreamHandler set a context value (e.g. via context.WithValue) that external clients cannot inject, and check for that context value in SkipsAuthentication instead of gRPC metadata. This removes the spoofing surface entirely.

🤖 Automated review by the Flipt PR review agent.

Adds an HTTP alias for the client evaluation SSE stream at
/ofrep/v1/_stream/{environmentKey}/{namespaceKey}/events that rewrites
to the existing /client/v2/environments/{key}/namespaces/{key}/stream.
The handler accepts only text/event-stream requests (406 otherwise),
sets an internal marker header for auth exclusion, and forces the
marshaler to eventSourceMarshaler.

Authentication respects authentication.exclude.ofrep: the stream
endpoint skips auth only when the config is enabled, the internal
marker is present, and the call originated through the in-process
gRPC gateway channel (not direct gRPC). A global incoming header
matcher rejects the marker header from clients to prevent spoofing.

The OFREP bulk evaluation response now advertises the new path.

Signed-off-by: Roman Dmytrenko <[email protected]>
@erka
erka force-pushed the rd/v2/ofrep-schema-change branch from af10b31 to 08903c7 Compare June 3, 2026 13:01

@github-actions github-actions 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.

Verdict: approve

Clean, focused PR. The OFREP stream endpoint alias correctly uses an unspoofable context-bound signal to conditionally skip authentication, the CORS changes properly expose ETag and allow If-None-Match for cross-origin conditional requests, and the wire-compatible proto change to make origin optional aligns with the client expectations. Tests cover the new alias routing, auth skipping logic, and CORS behavior. No issues found.

🤖 Automated review by the Flipt PR review agent.

@erka
erka requested a review from markphelps June 3, 2026 17:00

@markphelps markphelps left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm!

@erka erka changed the title feat(ofrep): remove origin field and snapshotDigest from bulk evaluation fix(ofrep): new SSE event stream endpoint and conditional request support Jun 3, 2026
@erka
erka merged commit dba660d into v2 Jun 3, 2026
49 of 51 checks passed
@erka
erka deleted the rd/v2/ofrep-schema-change branch June 3, 2026 21:48
@github-project-automation github-project-automation Bot moved this to Done in Flipt V2 Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files. v2 Flipt v2

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants