Skip to content

Add CORS configuration for Admin SPA#1330

Merged
damianlegawiec merged 6 commits into
mainfrom
feature/rack-cors
Apr 28, 2026
Merged

Add CORS configuration for Admin SPA#1330
damianlegawiec merged 6 commits into
mainfrom
feature/rack-cors

Conversation

@damianlegawiec
Copy link
Copy Markdown
Member

@damianlegawiec damianlegawiec commented Apr 24, 2026

Summary

  • Add rack-cors middleware configuration for Admin SPA cross-origin access to /api/v3/admin/* endpoints
  • Origins dynamically validated against Spree::AllowedOrigin records with a 5-minute cache (Rails.cache)
  • Explicit header allowlist: Content-Type, Authorization, Accept, X-Requested-With, X-Spree-Api-Key

Test plan

  • Verify CORS preflight (OPTIONS) returns correct headers for an origin in Spree::AllowedOrigin
  • Verify requests from unlisted origins are rejected (no CORS headers)
  • Verify Admin SPA can successfully call /api/v3/admin/ endpoints cross-origin
  • Verify non-admin paths (e.g. /api/v3/store/) do not get CORS headers
  • Verify cached origin lookup expires after 5 minutes and picks up new/removed origins

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Enabled CORS for the admin API to allow secure cross-origin requests with explicit methods, headers, and credential support.
    • Added origin validation (format and size checks), short-lived caching of allow decisions, and error logging for validation failures to improve security, performance, and observability.

Enable cross-origin requests from allowed origins to the Admin API v3
endpoints. Origins are dynamically checked against Spree::AllowedOrigin
records with a 5-minute cache to avoid per-request DB queries. Scoped
to /api/v3/admin/* with an explicit header allowlist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@damianlegawiec has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 42 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 88aa51bf-89fe-4470-ab68-081d76384221

📥 Commits

Reviewing files that changed from the base of the PR and between 75015a2 and 4faab1a.

📒 Files selected for processing (1)
  • config/initializers/cors.rb

Walkthrough

Adds the rack-cors gem and a new Rails initializer that mounts Rack::Cors for /api/v3/admin/*, validates origin format/length, caches allow decisions in Rails.cache for 5 minutes under cors/allowed_origin:<origin>, checks Spree::AllowedOrigin.exists?(origin: source), logs lookup errors, and enables credentialed requests.

Changes

Cohort / File(s) Summary
Dependency Management
Gemfile
Added gem 'rack-cors' to include CORS middleware dependency.
CORS Configuration
config/initializers/cors.rb
New initializer inserting Rack::Cors at the top of the middleware stack; normalizes/validates origins (rejects blank, >253 bytes, or non-http(s) URIs), caches allow decisions for 5 minutes under cors/allowed_origin:<source> (dev vs non-dev keys differ), queries Spree::AllowedOrigin.exists?(origin: source), rescues/logs StandardError during checks, and applies CORS rules to /api/v3/admin/* permitting listed headers, methods (including OPTIONS), and credentials.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RackCors as Rack::Cors
  participant RailsCache as Rails.cache
  participant DB as Spree::AllowedOrigin
  Client->>RackCors: Request to /api/v3/admin/...
  RackCors->>RackCors: Extract Origin header
  RackCors->>RackCors: Validate format & length
  RackCors->>RailsCache: fetch("cors/allowed_origin:<origin>")
  alt cached
    RailsCache-->>RackCors: cached decision (allow/deny)
  else not cached
    RailsCache->>DB: exists?(origin: <origin>)
    DB-->>RailsCache: boolean
    RailsCache-->>RackCors: store & return decision
  end
  alt allowed
    RackCors-->>Client: Proceed (CORS headers, credentials allowed)
  else denied
    RackCors-->>Client: Block or no CORS headers
  end
  rect rgba(0,128,0,0.5)
    Note over RackCors,DB: Errors during DB check are logged to Rails.logger
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I peek at origins, quick and spry,
I cache their fate beneath the sky,
Five minutes' pause, then verdicts told,
Admin paths guarded, bold not cold,
Hoppity-hop — headers unfold.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add CORS configuration for Admin SPA' accurately summarizes the main change—adding CORS middleware configuration for an Admin SPA, which is the core purpose of both the Gemfile addition and the new initializer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/rack-cors

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
config/initializers/cors.rb (1)

3-7: Nit: multiline lambda style.

Rubocop Style/Lambda flags line 3. Prefer the lambda keyword for multiline lambdas (or collapse into the origins call). Purely cosmetic.

💅 Proposed change
-    origins ->(source, _env) {
-      Rails.cache.fetch("cors/allowed_origin:#{source}", expires_in: 5.minutes) do
-        Spree::AllowedOrigin.exists?(origin: source)
-      end
-    }
+    origins lambda { |source, _env|
+      Rails.cache.fetch("cors/allowed_origin:#{source}", expires_in: 5.minutes) do
+        Spree::AllowedOrigin.exists?(origin: source)
+      end
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/initializers/cors.rb` around lines 3 - 7, The multiline lambda passed
to origins should use the lambda keyword and do/end form instead of the ->
literal; update the origins handler (the current ->(source, _env) {
Rails.cache.fetch("cors/allowed_origin:#{source}", expires_in: 5.minutes) {
Spree::AllowedOrigin.exists?(origin: source) } }) to use lambda do |source,
_env| ... end around the Rails.cache.fetch block so RuboCop Style/Lambda is
satisfied while preserving the Rails.cache.fetch and
Spree::AllowedOrigin.exists? logic.
Gemfile (1)

51-51: Optional: pin rack-cors to a known-good major version.

Security middleware gems warrant explicit version control; consider using a constraint like '~> 2.0' to make upgrades intentional and prevent unexpected breaking changes to CORS semantics. The latest release (3.0.0) illustrates why unpinned dependencies can drift across major versions. Not blocking—other gems in this file follow the same pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Gemfile` at line 51, The Gemfile currently lists gem 'rack-cors' without a
version; pin it to a known-good major version to avoid accidental breaking
upgrades by changing the Gemfile entry for 'rack-cors' to include a version
constraint (for example '~> 2.0' or another approved major version) so Bundler
will keep upgrades intentional and prevent automatic pulls of incompatible v3.x
semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/initializers/cors.rb`:
- Around line 1-12: The origins lambda in config/initializers/cors.rb currently
calls Rails.cache.fetch and Spree::AllowedOrigin.exists? which can raise and
cause 5xx responses for all /api/v3/admin/* requests; modify the origins
->(source, _env) { ... } block to rescue storage/DB errors (e.g., StandardError)
around the Rails.cache.fetch / Spree::AllowedOrigin.exists? call, log the error
via Rails.logger.error with context (include source and the exception), and
return false (fail closed) so transient Redis/DB errors deny the origin rather
than crashing the CORS middleware.
- Around line 3-7: The origins lambda currently uses the raw request Origin as
the cache key and DB lookup (Rails.cache.fetch("cors/allowed_origin:#{source}"
...), Spree::AllowedOrigin.exists?(origin: source)), which allows unbounded
cache key growth; before calling Rails.cache.fetch or
Spree::AllowedOrigin.exists? validate and reject attacker-controlled inputs by
returning false for nil/blank values, enforce a reasonable byte-length cap
(e.g., a few hundred bytes), and only accept syntactically valid origins with
http or https schemes (parse and validate the origin); for rejected/invalid
origins skip the cache/DB path and optionally write/lookup a single coarse
negative-cache key (e.g., "cors/allowed_origin:invalid") to collapse repeated
bad requests.

---

Nitpick comments:
In `@config/initializers/cors.rb`:
- Around line 3-7: The multiline lambda passed to origins should use the lambda
keyword and do/end form instead of the -> literal; update the origins handler
(the current ->(source, _env) {
Rails.cache.fetch("cors/allowed_origin:#{source}", expires_in: 5.minutes) {
Spree::AllowedOrigin.exists?(origin: source) } }) to use lambda do |source,
_env| ... end around the Rails.cache.fetch block so RuboCop Style/Lambda is
satisfied while preserving the Rails.cache.fetch and
Spree::AllowedOrigin.exists? logic.

In `@Gemfile`:
- Line 51: The Gemfile currently lists gem 'rack-cors' without a version; pin it
to a known-good major version to avoid accidental breaking upgrades by changing
the Gemfile entry for 'rack-cors' to include a version constraint (for example
'~> 2.0' or another approved major version) so Bundler will keep upgrades
intentional and prevent automatic pulls of incompatible v3.x semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 548d05e0-c52f-4eb5-8a35-fe5e4e27e155

📥 Commits

Reviewing files that changed from the base of the PR and between cf29c68 and b9d3390.

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • Gemfile
  • config/initializers/cors.rb

Comment thread config/initializers/cors.rb
Comment thread config/initializers/cors.rb Outdated
- Validate origin format before cache/DB lookup (reject blank, oversized,
  or non-HTTP(S) origins) to prevent cache pollution
- Rescue cache/DB errors and fail closed (deny origin) instead of 500
- Use lambda keyword for multiline lambda (RuboCop Style/Lambda)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/initializers/cors.rb`:
- Around line 3-13: The brace-delimited lambda assigned to origins uses an
inline rescue which is a syntax error; change the lambda from brace form to a
do...end block so the rescue can be used legally. Locate the origins lambda (the
block that checks source.blank?, source.bytesize, the regex match, and the
Rails.cache.fetch("cors/allowed_origin:#{source}", ...) call) and rewrite it as
lambda do |source, _env| ... end, keeping the Rails.cache.fetch block and the
rescue StandardError => e that logs via Rails.logger.error and returns false in
the rescue branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 363542a9-92aa-4473-af2e-3f26401efc36

📥 Commits

Reviewing files that changed from the base of the PR and between b9d3390 and 6672e53.

📒 Files selected for processing (1)
  • config/initializers/cors.rb

Comment thread config/initializers/cors.rb Outdated
Ruby does not allow rescue inside brace-delimited blocks. Switch
the origins lambda from { } to do...end so the rescue clause parses
correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`origins lambda do...end` causes Ruby to bind the do...end block to
`origins` instead of `lambda`, resulting in ArgumentError. Extract
the lambda to a local variable to avoid the ambiguous block binding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In development, normalize the request Origin to scheme+host (dropping
port) before checking against AllowedOrigin records. This lets the
Admin SPA on http://localhost:5173 share an AllowedOrigin entry with
the storefront on http://localhost:3001 without configuring every dev
port. Production keeps strict exact-match behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/initializers/cors.rb`:
- Around line 15-25: The allowlist lookup and cache in the CORS initializer
currently use only origin (variables source/normalized_source) so
Spree::AllowedOrigin.pluck/exists? and Rails.cache.fetch can return an allowed
origin for one store to other stores; update both branches to include the
current store id in the DB query and cache key (e.g. include store_id alongside
origin) so that the Spree::AllowedOrigin lookup (used via
Spree::AllowedOrigin.pluck/exists?) is filtered by store_id and the
Rails.cache.fetch key is suffixed with the store id (use
normalize_origin.call(source)/normalized_source as before but add store id to
the cache key and the ActiveRecord query).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cccdaa8-4bc3-4da8-8d8f-8c6f9909dc1e

📥 Commits

Reviewing files that changed from the base of the PR and between c078a99 and 75015a2.

📒 Files selected for processing (1)
  • config/initializers/cors.rb

Comment thread config/initializers/cors.rb
The Spree SDK and browsers send headers like Idempotency-Key and DNT
that were not in the explicit allowlist, causing CORS preflight
failures. Use :any to reflect requested headers — the security
boundary is the origin allowlist and server-side API key auth, not
the header list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@render render Bot temporarily deployed to feature/rack-cors - Silver Ghost static cloud service 001 PR #1330 April 28, 2026 11:57 Destroyed
@damianlegawiec damianlegawiec merged commit d3fb954 into main Apr 28, 2026
6 checks passed
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