Skip to content

fix(op): isolate provider endpoints and CORS options from package globals - #927

Open
lewis-treacy wants to merge 1 commit into
zitadel:mainfrom
lewis-treacy:lewis/isolate-provider-endpoints-from-globals
Open

fix(op): isolate provider endpoints and CORS options from package globals#927
lewis-treacy wants to merge 1 commit into
zitadel:mainfrom
lewis-treacy:lewis/isolate-provider-endpoints-from-globals

Conversation

@lewis-treacy

Copy link
Copy Markdown

Which Problems Are Solved

  • Provider.endpoints was a *Endpoints pointing at the package-global DefaultEndpoints, so WithCustomAuthEndpoint and the other WithCustom*Endpoint options assigned through the pointer into the global rather than into the provider. A custom endpoint set on one provider appeared on every other provider constructed in the same process, including providers that never asked for it.
  • Those writes are unsynchronised, so constructing a provider with custom endpoints while another provider is already serving requests is a data race on the shared Endpoints. go test -race reports it on (*Provider).TokenEndpoint().
  • Scopes() and SupportedClaims() returned the exported DefaultSupportedScopes and DefaultSupportedClaims slices directly, so a caller that mutates the returned slice edits the package defaults for the whole process.

How the Problems Are Solved

  • Provider.endpoints is now an Endpoints value instead of a *Endpoints. NewProvider copies the defaults and each WithCustom*Endpoint assigns into the provider's own copy. The exported DefaultEndpoints keeps its *Endpoints type, so there is no API change.
  • NewProvider copies defaultCORSOptions per provider. CORSOptions() returns that pointer to callers, so while the global was shared, p.CORSOptions().AllowedOrigins = ... reconfigured CORS for every provider in the process.
  • Scopes() and SupportedClaims() return slices.Clone(...).

Additional Changes

  • RegisterServer also copies defaultCORSOptions instead of taking the address of the global. There is no reachable mutation path today, because WithServerCORSOptions replaces the pointer and webServer exposes no accessor. It removes the last alias to the global and keeps both constructors consistent.
  • Two regression tests, both of which fail on main:
    • TestNewProviderDoesNotMutateDefaultEndpoints fails with sibling provider inherited a customisation it did not request.
    • TestNewProviderEndpointsConcurrentRace trips the race detector.

Additional Context

  • Setting op.DefaultEndpoints before constructing a provider still works as a process-wide default. It no longer retroactively mutates providers that already exist, which is the racy behaviour being removed.
  • Endpoint customisations no longer propagate between providers. Anyone relying on that will see a change in behaviour.

…bals

Provider.endpoints was a *Endpoints pointing at the package-global
DefaultEndpoints, so every WithCustom*Endpoint option wrote through into the
global. Custom endpoints leaked into other providers built in the same
process, and those writes raced with reads on a provider already serving
requests.

Store endpoints by value so each provider copies the defaults and customises
its own copy. Copy defaultCORSOptions per provider for the same reason, since
CORSOptions() hands its pointer to callers. Return copies from Scopes() and
SupportedClaims() so callers cannot edit the exported default slices in place.

Add regression tests for the cross-provider leak and the data race.
Comment thread pkg/op/op.go
issuer IssuerFromRequest
insecure bool
endpoints *Endpoints
endpoints Endpoints

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This changes the public contract and thus is a breaking change. We need to either keep it a pointer but copy the values, or merge this into next instead of main.

Copilot AI 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.

Pull request overview

This PR fixes unintended cross-provider sharing of mutable defaults in the OP package by copying default endpoints/CORS options into each newly constructed provider and by preventing callers from mutating package-default slices via returned values.

Changes:

  • Make Provider.endpoints a per-provider Endpoints value copied from DefaultEndpoints, so WithCustom*Endpoint no longer mutates global defaults or other providers.
  • Copy CORS options per provider/server to avoid global CORS reconfiguration via returned pointers.
  • Return cloned scope/claim slices from discovery helpers and add regression tests for endpoint isolation and a former race scenario.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
pkg/op/op.go Copies default endpoints/CORS options into each provider; updates Provider.endpoints to a value.
pkg/op/server_http.go Copies default server CORS options per registered server instance.
pkg/op/discovery.go Uses slices.Clone to prevent callers from mutating default supported scopes/claims.
pkg/op/op_test.go Adds regression tests for default endpoint mutation and a former race scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/op/server_http.go
decoder := schema.NewDecoder()
decoder.IgnoreUnknownKeys(true)

corsOpts := defaultCORSOptions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would like to see a copy/clone function for that so that this behavior is isolated.

Comment thread pkg/op/op_test.go
Comment thread pkg/op/op.go
Comment on lines +272 to +273
// Copied per provider: the options below assign into these and CORSOptions() hands its pointer to callers, so sharing the globals would let one provider reconfigure every other.
corsOpts := defaultCORSOptions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would like to see a copy/clone function for that so that this behavior is isolated.

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.

3 participants