spree_api 5.6.0.rc5 → 5.6.0.rc6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eee15468ad97bc5af5ed27d4e0ed4f4149c8b325efe81d9ef05d91af84094a27
4
- data.tar.gz: c5d97420fb0bf65903f944487e102a57f957babdb7119dc2406307c0d45b2634
3
+ metadata.gz: 9f86ce456fb13e7e6f1fac5a39a1ad7f0a4befe5dc959a88c80b3bcaa3689b57
4
+ data.tar.gz: 4df7f39849201749a3b5a26337a009a36ab0b9fc4b4588d5d90accf859368ec7
5
5
  SHA512:
6
- metadata.gz: e7783e8de7f1681b747863d5f9d161e1c5b8d05e1c087b5454332868fa81caaf7900e421fb60dabcca24b1d8ffbb99abf68338b0e85f13d6f456708e8c35e40f
7
- data.tar.gz: 30aad9e4cc778ae8d11b1527053cf1f96f4fc75846ee48422152939a050a2506537824c1ffbeafc0b53dd8bd7c9c927ff39f5014ef462b9581c2f66a177d6ee7
6
+ metadata.gz: 7fa240df22e6815d24910c37e32b873a8dae7a4e0ded6c00430e7d895e287ae3ae6e78cb11fcbb52d4930f876e48e22bf8be7353eb317722789772a17ceb6aa9
7
+ data.tar.gz: b4623924dc7d63bb2abae38790fa31c5140392e6838cce8c1c530fd6314110856fcf55c8c5c8a2c01a81fff58a1a7106787a3f3615d1b0f0dd16af4b911c027c
@@ -22,10 +22,13 @@ module Spree
22
22
  current_user.nil?
23
23
  end
24
24
 
25
- # Set Vary headers to ensure proper CDN caching by currency/locale
25
+ # Set Vary headers to ensure proper CDN caching by currency/locale/channel.
26
+ # X-Spree-Channel is included because the resolved channel changes the
27
+ # serialized body (price hiding + channel-scoped visibility), so a shared
28
+ # cache must not serve one channel's guest response to another channel.
26
29
  def set_vary_headers
27
30
  if guest_user?
28
- response.headers['Vary'] = 'Accept, x-spree-currency, x-spree-locale'
31
+ response.headers['Vary'] = 'Accept, x-spree-currency, x-spree-locale, x-spree-channel'
29
32
  else
30
33
  response.headers['Cache-Control'] = 'private, no-store'
31
34
  end
@@ -67,14 +70,21 @@ module Spree
67
70
 
68
71
  expires_in expires_in, public: true
69
72
 
70
- # Use Rails' stale? which handles ETag and Last-Modified
71
- stale?(resource, public: true)
73
+ # Use Rails' stale? which handles ETag and Last-Modified. The channel
74
+ # is folded into the ETag so a shared cache never serves one channel's
75
+ # guest response to another; Last-Modified mirrors the resource's own
76
+ # timestamp as before.
77
+ stale?(
78
+ etag: [resource, cache_channel_fragment],
79
+ last_modified: resource.try(:updated_at),
80
+ public: true
81
+ )
72
82
  end
73
83
 
74
84
  private
75
85
 
76
86
  # Build a cache key for a collection
77
- # Includes: latest updated_at, total count, query params, pagination, expand, currency, locale
87
+ # Includes: latest updated_at, total count, query params, pagination, expand, currency, locale, channel
78
88
  def collection_cache_key(collection)
79
89
  # For ActiveRecord collections use updated_at, for plain arrays use store's updated_at as proxy
80
90
  latest_updated_at = if collection.first&.respond_to?(:updated_at)
@@ -92,11 +102,22 @@ module Spree
92
102
  params[:page],
93
103
  params[:limit],
94
104
  current_currency,
95
- current_locale
105
+ current_locale,
106
+ cache_channel_fragment
96
107
  ]
97
108
 
98
109
  parts.compact.join('/')
99
110
  end
111
+
112
+ # Identifies the resolved channel for cache-key purposes. The channel
113
+ # changes the serialized body (price hiding + channel-scoped visibility),
114
+ # so it must be part of the guest cache identity. Uses the same
115
+ # +current_channel+ source of truth as serialization/gating.
116
+ def cache_channel_fragment
117
+ return nil unless respond_to?(:current_channel, true)
118
+
119
+ current_channel&.id
120
+ end
100
121
  end
101
122
  end
102
123
  end
@@ -56,10 +56,34 @@ module Spree
56
56
  private
57
57
 
58
58
  def authorize_api_key_scope!
59
- return unless current_api_key
59
+ # Endpoints explicitly exempt from the scope mechanism (auth, me, public
60
+ # invitation acceptance, per-type-filtered exports, etc.) opt out first,
61
+ # before any key resolution or enforcement.
60
62
  return if self.class._scope_check_skipped
61
63
  return if self.class._scope_check_skipped_actions&.include?(action_name)
62
64
 
65
+ # Fail CLOSED, not open. A nil `current_api_key` legitimately means a
66
+ # non-secret-key request (JWT admin / publishable) authorized by other
67
+ # means, which correctly skips scope checks. But it ALSO happens when
68
+ # this guard runs before authentication resolved the key: a bare
69
+ # `return unless current_api_key` would then skip scope enforcement for
70
+ # an attacker-supplied secret key. So resolve the secret key in place
71
+ # here — never rely on before_action ordering. Only truly non-secret-key
72
+ # requests (nothing to resolve) are waved through.
73
+ resolve_current_api_key_for_scope_check!
74
+
75
+ unless current_api_key
76
+ return unless secret_key_request?
77
+
78
+ # A secret-key token was presented but did not resolve to a live key
79
+ # for this store — reject rather than silently skip the scope check.
80
+ return render_error(
81
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:invalid_token],
82
+ message: 'Valid secret API key required',
83
+ status: :unauthorized
84
+ )
85
+ end
86
+
63
87
  resource = scoped_resource_name
64
88
  # Fail closed: a controller authenticated by API key MUST declare
65
89
  # either `scoped_resource :name`, override `scoped_resource_name`,
@@ -77,6 +101,29 @@ module Spree
77
101
  )
78
102
  end
79
103
 
104
+ # True when the request presents a secret API key token (`sk_` prefix),
105
+ # regardless of whether authentication has resolved it into
106
+ # `current_api_key` yet. Used to keep the scope guard fail-closed even if
107
+ # it runs before the API key is loaded: a secret-key request whose key is
108
+ # still nil must be denied, never waved through.
109
+ def secret_key_request?
110
+ extract_api_key.to_s.start_with?(Spree::ApiKey::PREFIXES['secret'])
111
+ end
112
+
113
+ # Resolves the request's secret API key into `@current_api_key` if a
114
+ # secret-key token is present and it hasn't been resolved yet, so the scope
115
+ # check does not depend on `authenticate_admin!` having already run. Mirrors
116
+ # the secret-key resolution + store binding in AdminAuthentication, and is
117
+ # idempotent: when the key is already loaded (guard runs after auth) it does
118
+ # nothing.
119
+ def resolve_current_api_key_for_scope_check!
120
+ return if @current_api_key
121
+ return unless secret_key_request?
122
+
123
+ key = Spree::ApiKey.find_by_secret_token(extract_api_key)
124
+ @current_api_key = key if key && key.store_id == current_store.id
125
+ end
126
+
80
127
  # The resource name used in scope strings (`read_<name>` / `write_<name>`).
81
128
  # Defaults to the class-level `scoped_resource :name` declaration.
82
129
  # Override in controllers that resolve scope at request time (e.g. the
@@ -4,9 +4,13 @@ module Spree
4
4
  module Admin
5
5
  class BaseController < Spree::Api::V3::BaseController
6
6
  include Spree::Api::V3::AdminAuthentication
7
- include Spree::Api::V3::ScopedAuthorization
8
7
 
8
+ # Must be registered before ScopedAuthorization so authenticate_admin!
9
+ # resolves @current_api_key before authorize_api_key_scope! reads it —
10
+ # otherwise the scope guard short-circuits on a nil key and never runs.
9
11
  before_action :authenticate_admin!
12
+
13
+ include Spree::Api::V3::ScopedAuthorization
10
14
  end
11
15
  end
12
16
  end
@@ -60,15 +60,24 @@ module Spree
60
60
  false
61
61
  end
62
62
 
63
- # Tagging filter. `Spree::Order` carries a `tenant` (store_id) column
64
- # via `acts_as_taggable_tenant`, so its tag vocabulary is bounded to
65
- # the current store; other taggables fall back to the type filter.
63
+ # Tagging filter. Store-owned taggables (`Spree::Product`,
64
+ # `Spree::Order`) tenant their taggings by `store_id` via
65
+ # `acts_as_taggable_tenant`, so their vocabulary is bounded to the
66
+ # current store with a single indexed `tenant` filter. Global
67
+ # taggables like the customer/user type carry no store, so their tags
68
+ # are not store-scoped here.
66
69
  def taggings_conditions
67
70
  conditions = { taggable_type: taggable_type, context: 'tags' }
68
- conditions[:tenant] = current_store.id.to_s if taggable_type == 'Spree::Order'
71
+ conditions[:tenant] = current_store.id.to_s if store_tenanted_taggable?
69
72
  conditions
70
73
  end
71
74
 
75
+ # True for taggables whose taggings carry a store `tenant`
76
+ # (`acts_as_taggable_tenant :store_id`), i.e. store-owned records.
77
+ def store_tenanted_taggable?
78
+ %w[Spree::Product Spree::Order].include?(taggable_type)
79
+ end
80
+
72
81
  # Per-type scope check for API-key principals: a key listing product
73
82
  # tags needs `read_products`, order tags `read_orders`, etc. JWT
74
83
  # admins are gated by store membership + CanCanCan, not scopes.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0.rc5
4
+ version: 5.6.0.rc6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-22 00:00:00.000000000 Z
11
+ date: 2026-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 5.6.0.rc5
75
+ version: 5.6.0.rc6
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 5.6.0.rc5
82
+ version: 5.6.0.rc6
83
83
  description: Spree's API
84
84
  email:
85
85
  - hello@spreecommerce.org
@@ -384,9 +384,9 @@ licenses:
384
384
  - BSD-3-Clause
385
385
  metadata:
386
386
  bug_tracker_uri: https://github.com/spree/spree/issues
387
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc5
387
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc6
388
388
  documentation_uri: https://docs.spreecommerce.org/
389
- source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc5
389
+ source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc6
390
390
  post_install_message:
391
391
  rdoc_options: []
392
392
  require_paths: