Skip to content

fix(concurrent-keys): reject SDK keys and mobile keys scoped to a view - #795

Draft
aaron-zeisler wants to merge 3 commits into
feat/concurrent-keysfrom
aaronz/SDK-2860/reject-view-scoped-keys
Draft

fix(concurrent-keys): reject SDK keys and mobile keys scoped to a view#795
aaron-zeisler wants to merge 3 commits into
feat/concurrent-keysfrom
aaronz/SDK-2860/reject-view-scoped-keys

Conversation

@aaron-zeisler

@aaron-zeisler aaron-zeisler commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the Relay Proxy reject SDK keys and mobile keys that are scoped to a view, so they never enter the accepted credential set. Filtering happens in envfactory.BuildAcceptedSet so a view-scoped key is absent from the credential lookup map entirely. An SDK presenting one gets a 401.

Background

A view-scoped key (payload filtering v2) is only entitled to a subset of its environment's flags. Relay serves the entire environment payload and has no view support, so accepting one would silently over-deliver every flag in the environment to an SDK that only wants a subset.

Commit details

The first two commits are the feature, the third is a cleanup task.

1. chore: carry the hasViews marker through the credential wire format — 3 files, +81/−13

Adds a hasViews flag to each SDK key and mobile key the backend sends us, and passes it inward to where relay assembles an environment's credentials. Nothing acts on it yet.

2. fix: reject SDK keys and mobile keys scoped to a view — 5 files, +271/−41

Discards view-scoped keys while the payload is being read, so they never join the set of credentials an environment will accept and an SDK presenting one is turned away as unrecognized. An environment's own default SDK key and mobile key are deliberately exempt, since refusing either of those would take the whole environment offline.

3. refactor: make every credential-set rejection a malformed-payload error — 7 files, +52/−46

When a payload left us with no usable SDK key, relay reported that as a different kind of error than it uses for other unusable payloads, which made offline mode log a misleading reason. They are now the same kind, so nothing downstream has to tell them apart.

Integration coverage

Integration tests for this change are stacked in #796 .

Open questions for review

Should /status show the rejected view-scoped keys?, so support can diagnose without asking the customer for logs? Deliberately excluded here: it would require carrying view-scoped values into EnvContext, reintroducing exactly the state the ingestion filter exists to avoid, and would add a field to a rep that was only just stabilized.

Comment thread internal/autoconfig/stream_manager.go
Comment thread internal/envfactory/env_params.go Outdated
Comment thread internal/envfactory/env_params.go Outdated
Comment thread internal/envfactory/env_rep.go Outdated
Comment thread internal/envfactory/reconcile_helper.go
Comment thread internal/envfactory/reconcile_helper.go Outdated
Comment thread internal/envfactory/reconcile_helper.go Outdated
Comment thread relay/autoconfig_actions.go Outdated
Comment thread internal/autoconfig/stream_manager.go Outdated
Comment thread internal/envfactory/env_rep.go Outdated
…ial wire format

Adds hasViews to each sdkKeys[]/mobileKeys[] entry and carries it onto the
accepted-key params. Nothing reads it yet — the filter that consumes it
follows in the next commit.

A plain bool rather than a pointer: an absent field means "not view-scoped"
and there is no third state, unlike expiry where nil genuinely means
permanent. Old-format payloads synthesize their accepted keys from the
singular sdkKey/mobKey and so never set it, which is correct — the backend
forbids views on a default key.
The sdkKeys[]/mobileKeys[] arrays hand relay every non-default credential
for an environment, including keys scoped to a view. A view-scoped key is
only entitled to a subset of the environment's flags, but relay serves the
whole environment payload and has no view support — so accepting one would
silently over-deliver every flag to an SDK that must see only a subset.

Filter them in BuildAcceptedSet, the single funnel for both the auto-config
stream and the offline archive. They never enter the accepted set, so an SDK
presenting one is rejected because the credential is absent from the lookup
map, and no state for it reaches /status, event forwarding, or the expiry
ticker. BuildAcceptedSet returns the identifiers it dropped so the handlers
that apply a set can WARN; it takes no logger because the stream parse
boundary validates every payload and would double-log each one.

Rejection is keyed by credential value rather than evaluated per entry. The
accepted-set builder is first-wins, so if two entries carry the same value
and only one is marked, per-entry filtering would admit the credential via
the unmarked entry while still reporting it rejected. Tainting the value
makes one marked entry sufficient whichever position it holds. Excluding the
designated key from the taint set is also what keeps the anchor guard
structural: the marker is disregarded on the anchor and the primary mobile
key, because dropping a designated key would take the whole environment down
and the backend forbids views on a default key in the first place.

Acquiring a view mid-session needed no new code: reconcileAcceptedKeys
already revokes any key absent from the desired set immediately rather than
on an expiry timestamp, and RemoveConnectionMapping unmaps before the
streams are torn down so a reconnect is rejected.
…ormed-payload error

AcceptedSetBuilder.Build returned a bare sentinel when no SDK key survived,
while every other rejection was a *MalformedCredentialSetError. That framed
an empty set as a caller mistake, which it no longer is: the payload reaches
it by combining an undefined anchor with an sdkKeys[] array that is either
empty or made up entirely of keys relay excludes — the view-scoped case being
new. Return the same type, with a message describing the payload rather than
the builder, since that is what an operator reading the log can act on.

With the type now uniform, the offline handlers no longer branch on it. Both
branches already preserved previous credentials and differed only in message
text, and the fallback text was the wrong one: it claimed the environment was
auto-configured, and in UpdateEnvironment that the environment failed to
initialize when it had already been running. Log the accurate message
unconditionally instead.

Stop documenting the error type as a contract. Nothing in production inspects
it now, and BuildAcceptedSet cannot honor such a promise on its own — the
guarantee would depend on a function in another package whose author has no
reason to know about it. The taxonomy stays pinned by tests, which is where a
claim like this can actually be enforced; TestBuildAcceptedSet_NoSDKKeys now
covers both shapes that reach the empty-set path, as it is the only test that
would catch this classification regressing.
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/reject-view-scoped-keys branch from 706ca7a to ffc86d5 Compare August 6, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant