spree_api 6.0.0.beta1 → 6.0.0.beta3

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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/embed_permissions.rb +55 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +34 -5
  4. data/app/controllers/concerns/spree/api/v3/admin/stock_receipt_actions.rb +1 -1
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +11 -0
  6. data/app/controllers/concerns/spree/api/v3/current_password_confirmation.rb +8 -0
  7. data/app/controllers/concerns/spree/api/v3/orders/claim_actions.rb +2 -2
  8. data/app/controllers/concerns/spree/api/v3/orders/exchange_actions.rb +3 -3
  9. data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +15 -2
  10. data/app/controllers/concerns/spree/api/v3/orders/post_sale_actions.rb +1 -1
  11. data/app/controllers/concerns/spree/api/v3/orders/return_actions.rb +3 -3
  12. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +14 -0
  13. data/app/controllers/concerns/spree/api/v3/store/storefront_products.rb +35 -0
  14. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +9 -0
  15. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +2 -2
  16. data/app/controllers/spree/api/v3/admin/base_controller.rb +1 -0
  17. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +8 -3
  18. data/app/controllers/spree/api/v3/admin/customers_controller.rb +0 -14
  19. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +29 -2
  20. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +4 -1
  21. data/app/controllers/spree/api/v3/admin/invitations/acceptance_links_controller.rb +41 -0
  22. data/app/controllers/spree/api/v3/admin/me_controller.rb +11 -0
  23. data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +14 -1
  24. data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +22 -1
  25. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
  26. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +1 -1
  27. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +22 -1
  28. data/app/controllers/spree/api/v3/admin/orders_controller.rb +41 -9
  29. data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +31 -11
  30. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +1 -19
  31. data/app/controllers/spree/api/v3/admin/prices_controller.rb +33 -2
  32. data/app/controllers/spree/api/v3/admin/products_controller.rb +1 -1
  33. data/app/controllers/spree/api/v3/admin/resource_controller.rb +1 -0
  34. data/app/controllers/spree/api/v3/admin/sellers/invitations/acceptance_links_controller.rb +37 -0
  35. data/app/controllers/spree/api/v3/admin/setup_controller.rb +9 -1
  36. data/app/controllers/spree/api/v3/admin/store_controller.rb +5 -0
  37. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +1 -1
  38. data/app/controllers/spree/api/v3/admin/translations_controller.rb +1 -1
  39. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +12 -0
  40. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +27 -0
  41. data/app/controllers/spree/api/v3/base_controller.rb +22 -3
  42. data/app/controllers/spree/api/v3/resource_controller.rb +27 -2
  43. data/app/controllers/spree/api/v3/seller/auth_controller.rb +1 -1
  44. data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +5 -2
  45. data/app/controllers/spree/api/v3/seller/invitations/acceptance_links_controller.rb +34 -0
  46. data/app/controllers/spree/api/v3/seller/me_controller.rb +25 -2
  47. data/app/controllers/spree/api/v3/seller/orders_controller.rb +20 -25
  48. data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +1 -1
  49. data/app/controllers/spree/api/v3/seller/resource_controller.rb +4 -0
  50. data/app/controllers/spree/api/v3/store/carts/items_controller.rb +7 -4
  51. data/app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb +10 -1
  52. data/app/controllers/spree/api/v3/store/carts/payments_controller.rb +5 -1
  53. data/app/controllers/spree/api/v3/store/companies/members_controller.rb +10 -11
  54. data/app/controllers/spree/api/v3/store/companies/orders_controller.rb +6 -0
  55. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +1 -0
  56. data/app/controllers/spree/api/v3/store/products_controller.rb +6 -12
  57. data/app/controllers/spree/api/v3/store/resource_controller.rb +4 -0
  58. data/app/controllers/spree/api/v3/store/wishlist_items_controller.rb +11 -0
  59. data/app/controllers/spree/api/v3/webhooks/fulfillments_controller.rb +10 -6
  60. data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +6 -2
  61. data/app/controllers/spree/api/v3/webhooks/payouts_controller.rb +6 -2
  62. data/app/jobs/spree/webhook_delivery_job.rb +18 -4
  63. data/app/serializers/spree/api/v3/admin/actor_serializer.rb +28 -0
  64. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +14 -1
  65. data/app/serializers/spree/api/v3/admin/claim_serializer.rb +3 -4
  66. data/app/serializers/spree/api/v3/admin/exchange_serializer.rb +3 -4
  67. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +6 -0
  68. data/app/serializers/spree/api/v3/admin/invitation_acceptance_link_serializer.rb +15 -0
  69. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +0 -11
  70. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +28 -1
  71. data/app/serializers/spree/api/v3/admin/order_group_serializer.rb +7 -0
  72. data/app/serializers/spree/api/v3/admin/order_serializer.rb +7 -24
  73. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +6 -0
  74. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -3
  75. data/app/serializers/spree/api/v3/admin/saved_report_serializer.rb +3 -4
  76. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +2 -3
  77. data/app/serializers/spree/api/v3/admin/store_serializer.rb +10 -0
  78. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -2
  79. data/app/serializers/spree/api/v3/base_serializer.rb +22 -0
  80. data/app/serializers/spree/api/v3/gift_card_serializer.rb +3 -1
  81. data/app/serializers/spree/api/v3/line_item_serializer.rb +2 -1
  82. data/app/serializers/spree/api/v3/market_serializer.rb +5 -0
  83. data/app/serializers/spree/api/v3/product_filters_serializer.rb +3 -0
  84. data/app/serializers/spree/api/v3/seller/account_serializer.rb +19 -0
  85. data/app/serializers/spree/api/v3/seller/invitation_acceptance_link_serializer.rb +15 -0
  86. data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +1 -10
  87. data/app/services/spree/api/v3/filters_aggregator.rb +5 -2
  88. data/config/routes.rb +7 -0
  89. data/lib/spree/api/configuration.rb +19 -14
  90. data/lib/spree/api/dependencies.rb +4 -0
  91. data/lib/spree/api/engine.rb +6 -0
  92. data/lib/spree/api/openapi/schema_helper.rb +10 -4
  93. metadata +15 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd13b5167236c50754b03e48d4e7ab4ff5f1c24cdfd13b5a1ea45ff7a2961dd7
4
- data.tar.gz: 77ab648d62775d22902a30ff8dbb4927fd0dab1b93668caad957c99a88bcd1fa
3
+ metadata.gz: d30acba1d9395e7575f92eefe7c86b5cd8c73eae44cceb1e178a853963e856c4
4
+ data.tar.gz: 562e93949b2a3e5b124c06896821d26554329f0e332faa8f9de2d35b6d40dc71
5
5
  SHA512:
6
- metadata.gz: 36a716f02b07837cf6360d19cb914ef28a1de1e5cef02b0d6cb503bcc11d89afa6f00f36bd1613d0e9de6962197af6830c8068d03963ca257ac6f59c7ab2fa95
7
- data.tar.gz: 388abb4801575690aa02e55a907a1851a1d88570a55681c60dc870f693a784024ed7d352e8fcedf493666bd2999269822e9d7abb579d13f3b8345ea9bb340ae2
6
+ metadata.gz: 401bbdc3ddb5eb121c8ad9eac1d5341aa130c0f1f61135e54b14c8eabe577c1845442f28e20e4bf608e35d5dc5f757b25b4b41697dd1010c78ae9f2ede161362
7
+ data.tar.gz: e27e19331d0e63339aa5bd21586f1f4ed74b095689a3c31252faee126f9d896f3579cef931a6fa66afa5b6e0c001f7759f19838f3a2914de105090f598b4c2ae
@@ -0,0 +1,55 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Keeps records another permission guards out of a response.
6
+ #
7
+ # An endpoint is gated on its own resource's key, but expanding an
8
+ # association pulls in whole records of another kind — a customer's
9
+ # orders, an order's payments, a gift card's customer. Without this a
10
+ # role cleared only for customers could read every order through
11
+ # `?expand=orders`. Each segment of an expand path is checked, so
12
+ # `orders.payments` needs both keys.
13
+ #
14
+ # Gift card codes are bearer credentials that ride along on every
15
+ # order, so serializers show them in full only to a caller who may read
16
+ # gift cards and masked to everyone else.
17
+ module EmbedPermissions
18
+ extend ActiveSupport::Concern
19
+
20
+ # Association name in an expand path => the catalog key it needs.
21
+ EXPANSION_PERMISSIONS = {
22
+ 'order' => 'read_orders',
23
+ 'orders' => 'read_orders',
24
+ 'payment' => 'read_payments',
25
+ 'payments' => 'read_payments',
26
+ 'payment_splits' => 'read_payments',
27
+ 'customer' => 'read_customers',
28
+ 'customers' => 'read_customers',
29
+ 'gift_card' => 'read_gift_cards',
30
+ 'gift_cards' => 'read_gift_cards',
31
+ 'store_credit' => 'read_store_credits',
32
+ 'store_credits' => 'read_store_credits'
33
+ }.freeze
34
+
35
+ protected
36
+
37
+ def expand_list
38
+ super.select { |path| path.split('.').all? { |segment| expansion_permitted?(segment) } }
39
+ end
40
+
41
+ def serializer_params
42
+ super.merge(gift_card_codes: holds_permission?('read_gift_cards'))
43
+ end
44
+
45
+ private
46
+
47
+ def expansion_permitted?(segment)
48
+ key = EXPANSION_PERMISSIONS[segment]
49
+ key.nil? || holds_permission?(key)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -23,13 +23,13 @@ module Spree
23
23
  # flow leaves it off (its gate is `manage Invitation`).
24
24
  # @return [Boolean] true if the request was rejected (caller should return)
25
25
  def reject_unauthorized_role_grant!(role_ids, require_role_management: false)
26
- return false if role_ids.blank?
27
-
28
26
  # The management gate runs whenever a role mutation is attempted —
29
- # before resolving ids — so passing unknown ids can't slip the
30
- # reconciliation (which would still remove the user's current roles)
31
- # past a caller without role-management authority.
27
+ # before resolving ids, and even for an empty list — so passing
28
+ # unknown ids, or none at all, can't slip the reconciliation (which
29
+ # would still remove the user's current roles) past a caller
30
+ # without role-management authority.
32
31
  return true if require_role_management && reject_without_role_management!
32
+ return false if role_ids.blank?
33
33
 
34
34
  roles = current_store.roles.where(id: role_ids.map(&:to_s)).to_a
35
35
  return false if roles.empty?
@@ -40,6 +40,30 @@ module Spree
40
40
  false
41
41
  end
42
42
 
43
+ # Removing a role is bounded like granting it: a caller may only take
44
+ # away authority they could hand out, so a staff member who can
45
+ # manage staff cannot strip the admin role from the store's owners.
46
+ # The store also keeps at least one admin, whoever asks.
47
+ #
48
+ # @param user [Object] whose roles are being removed
49
+ # @param roles [Array<Spree::Role>] the store roles being removed
50
+ # @return [Boolean] true if the request was rejected (caller should return)
51
+ def reject_unauthorized_role_removal!(user, roles)
52
+ return false if roles.empty?
53
+
54
+ if roles.any?(&:admin?)
55
+ return deny_role_grant!('You cannot remove the admin role.') unless caller_holds_admin_role?
56
+ return deny_role_grant!('The store must keep at least one admin.') if last_admin?(user)
57
+ end
58
+
59
+ return false if caller_holds_admin_role?
60
+
61
+ beyond_own = roles.reject { |role| excess_permission_keys(role.permissions).empty? }
62
+ return false if beyond_own.empty?
63
+
64
+ deny_role_grant!("You cannot remove roles beyond your own privileges: #{beyond_own.map(&:name).join(', ')}")
65
+ end
66
+
43
67
  # Rejects granting permission keys beyond the caller's own — used by
44
68
  # roles#create/#update on the requested `permissions` array.
45
69
  #
@@ -125,6 +149,11 @@ module Spree
125
149
  end
126
150
  end
127
151
 
152
+ def last_admin?(user)
153
+ admin_roles = current_store.roles.select(&:admin?)
154
+ Spree::RoleUser.where(role: admin_roles).where.not(user: user).none?
155
+ end
156
+
128
157
  def caller_holds_admin_role?
129
158
  return current_api_key.has_scope?('write_all') if scope_limited_principal?
130
159
 
@@ -50,7 +50,7 @@ module Spree
50
50
  received_at: params[:received_at],
51
51
  reference: params[:reference],
52
52
  notes: params[:notes],
53
- received_by: try_spree_current_user
53
+ received_by: current_actor
54
54
  }
55
55
  end
56
56
 
@@ -15,6 +15,17 @@ module Spree
15
15
  Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_ADMIN
16
16
  end
17
17
 
18
+ # A key-authenticated write records the key: it is the thing that gets
19
+ # named, scoped and revoked, so it is what an order's `canceler` or a
20
+ # receipt's `received_by` should point at. A JWT request records the
21
+ # admin. When both credentials are present the JWT user wins here too,
22
+ # matching how permissions resolve below.
23
+ #
24
+ # @return [Object, nil]
25
+ def current_actor
26
+ try_spree_current_user || @current_api_key
27
+ end
28
+
18
29
  # API-key-only requests bypass CanCanCan: the ScopedAuthorization
19
30
  # concern is the authoritative gate (read_/write_ scopes per resource).
20
31
  # JWT admin users keep CanCanCan abilities; if both credentials are
@@ -13,12 +13,20 @@ module Spree
13
13
 
14
14
  private
15
15
 
16
+ # Runs under the login lockout, so a stolen session cannot use this
17
+ # check to guess the password: misses count towards the lockout, and a
18
+ # locked account is refused even with the right password.
19
+ #
16
20
  # @return [Boolean] whether `current_password` matches the signed-in
17
21
  # account. False when the parameter is missing — an absent password
18
22
  # is not a correct one.
19
23
  def valid_current_password?
20
24
  return false if params[:current_password].blank?
21
25
 
26
+ Spree::Authentication::Lockout.check(current_user) { current_password_matches? } == :valid
27
+ end
28
+
29
+ def current_password_matches?
22
30
  if current_user.respond_to?(:valid_password?)
23
31
  current_user.valid_password?(params[:current_password])
24
32
  elsif current_user.respond_to?(:authenticate)
@@ -19,7 +19,7 @@ module Spree
19
19
 
20
20
  # PATCH .../claims/:id/approve
21
21
  def approve
22
- run_workflow(Spree.claim_approve_workflow, approver: try_spree_current_user)
22
+ run_workflow(Spree.claim_approve_workflow, approver: current_actor)
23
23
  end
24
24
 
25
25
  # PATCH .../claims/:id/resolve — a refund, a replacement, or both.
@@ -29,7 +29,7 @@ module Spree
29
29
  refund_method: params[:refund_method] || 'store_credit',
30
30
  amount: params[:amount],
31
31
  replacement_line_item_ids: replacement_line_item_ids,
32
- resolver: try_spree_current_user)
32
+ resolver: current_actor)
33
33
  end
34
34
 
35
35
  # PATCH .../claims/:id/deny
@@ -21,14 +21,14 @@ module Spree
21
21
 
22
22
  # PATCH .../exchanges/:id/approve
23
23
  def approve
24
- run_workflow(Spree.exchange_approve_workflow, approver: try_spree_current_user)
24
+ run_workflow(Spree.exchange_approve_workflow, approver: current_actor)
25
25
  end
26
26
 
27
27
  # PATCH .../exchanges/:id/receive
28
28
  def receive
29
29
  run_workflow(Spree.exchange_receive_workflow,
30
30
  items: items_for_receive,
31
- received_by: try_spree_current_user)
31
+ received_by: current_actor)
32
32
  end
33
33
 
34
34
  # PATCH .../exchanges/:id/fulfill — sends the replacement, settling
@@ -36,7 +36,7 @@ module Spree
36
36
  def fulfill
37
37
  run_workflow(Spree.exchange_fulfill_workflow,
38
38
  refund_method: params[:refund_method] || 'store_credit',
39
- refunder: try_spree_current_user)
39
+ refunder: current_actor)
40
40
  end
41
41
 
42
42
  # PATCH .../exchanges/:id/cancel
@@ -135,9 +135,22 @@ module Spree
135
135
  end
136
136
 
137
137
  # The attributes handed to the update workflow. The workflow assigns
138
- # ids raw, so a branch that must narrow one resolves it here first.
138
+ # ids raw, so the stock location is resolved first, through the same
139
+ # lookup create and split use.
139
140
  def update_attributes
140
- permitted_params.to_h
141
+ attributes = permitted_params.to_h
142
+
143
+ if attributes['stock_location_id'].present?
144
+ attributes['stock_location_id'] = find_stock_location!(attributes['stock_location_id']).id
145
+ end
146
+
147
+ attributes
148
+ end
149
+
150
+ # A stock location of the current store. A branch that must narrow it
151
+ # further (by ability, by seller) overrides this.
152
+ def find_stock_location!(id)
153
+ current_store.stock_locations.find_by_prefix_id!(id)
141
154
  end
142
155
 
143
156
  # What may be split out of this parcel, and where the split half
@@ -59,7 +59,7 @@ module Spree
59
59
  items: items_for_create,
60
60
  reason: reason_for_create,
61
61
  memo: create_params[:memo],
62
- created_by: try_spree_current_user,
62
+ created_by: current_actor,
63
63
  **arguments
64
64
  )
65
65
 
@@ -21,14 +21,14 @@ module Spree
21
21
 
22
22
  # PATCH .../returns/:id/approve
23
23
  def approve
24
- run_workflow(Spree.return_approve_workflow, approver: try_spree_current_user)
24
+ run_workflow(Spree.return_approve_workflow, approver: current_actor)
25
25
  end
26
26
 
27
27
  # PATCH .../returns/:id/receive — what the warehouse actually counted.
28
28
  def receive
29
29
  run_workflow(Spree.return_receive_workflow,
30
30
  items: items_for_receive,
31
- received_by: try_spree_current_user)
31
+ received_by: current_actor)
32
32
  end
33
33
 
34
34
  # PATCH .../returns/:id/refund
@@ -36,7 +36,7 @@ module Spree
36
36
  run_workflow(Spree.return_refund_workflow,
37
37
  amount: params[:amount],
38
38
  refund_method: params[:refund_method] || 'original_payment',
39
- refunder: try_spree_current_user)
39
+ refunder: current_actor)
40
40
  end
41
41
 
42
42
  # PATCH .../returns/:id/cancel
@@ -204,6 +204,20 @@ module Spree
204
204
  def scope_limited_principal?
205
205
  current_api_key.present? && current_user.blank?
206
206
  end
207
+
208
+ # Whether the caller holds a catalog key beyond the one the action
209
+ # needs — for data another permission guards riding along in a
210
+ # response (an expanded association, an embedded record). Mirrors the
211
+ # key gate's two principals: a secret key carries scopes, a signed-in
212
+ # staffer carries their roles' catalog keys.
213
+ def holds_permission?(key)
214
+ return current_api_key.has_scope?(key) if scope_limited_principal?
215
+
216
+ ability = current_ability
217
+ return true unless ability.respond_to?(:permission_keys)
218
+
219
+ ability.permission_keys.include?(key.to_s)
220
+ end
207
221
  end
208
222
  end
209
223
  end
@@ -0,0 +1,35 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ # The products a storefront buyer may see: available now (or on
6
+ # pre-order) in the current currency, narrowed to the catalogs their
7
+ # company, customer group or channel resolve to. The product listing
8
+ # reads through it, and so does anything else that accepts a product
9
+ # or variant from the buyer, so an id cannot reach what the listing
10
+ # would never show.
11
+ module StorefrontProducts
12
+ extend ActiveSupport::Concern
13
+
14
+ private
15
+
16
+ # @param base [ActiveRecord::Relation] products to narrow; the store's by default
17
+ # @return [ActiveRecord::Relation<Spree::Product>]
18
+ def storefront_products(base = current_store.products)
19
+ Spree.products_for_context_service.call(
20
+ store: current_store,
21
+ channel: current_channel,
22
+ customer: current_user,
23
+ base: base.available(Time.current, Spree::Current.currency, include_preorderable: true)
24
+ ).value
25
+ end
26
+
27
+ # @return [ActiveRecord::Relation<Spree::Variant>]
28
+ def storefront_variants
29
+ Spree::Variant.where(product_id: storefront_products.reorder(nil).select(Spree::Product.arel_table[:id]))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -24,6 +24,8 @@ module Spree
24
24
  # the account globally. The user keeps access to any other stores.
25
25
  def destroy
26
26
  authorize!(:destroy, @resource)
27
+ return if reject_unauthorized_role_removal!(@resource, @resource.spree_roles.for_resource(current_store).to_a)
28
+
27
29
  @resource.role_users.where(role: current_store.roles).destroy_all
28
30
  head :no_content
29
31
  end
@@ -42,6 +44,7 @@ module Spree
42
44
  # RoleManagement permission set, not by profile-edit rights.
43
45
  role_ids = role_ids_param if params.key?(:role_ids)
44
46
  return if role_ids && reject_unauthorized_role_grant!(role_ids, require_role_management: true)
47
+ return if role_ids && reject_unauthorized_role_removal!(@resource, removed_roles(role_ids))
45
48
 
46
49
  if @resource.update(identity_params)
47
50
  apply_role_ids(role_ids) if role_ids
@@ -89,6 +92,12 @@ module Spree
89
92
  ids.map { |id| Spree::PrefixedId.prefixed_id?(id) ? Spree::PrefixedId.decode_prefixed_id(id) : id }.compact
90
93
  end
91
94
 
95
+ # The store roles the user holds that `desired_role_ids` leaves out.
96
+ def removed_roles(desired_role_ids)
97
+ target = desired_role_ids.map(&:to_s)
98
+ @resource.spree_roles.for_resource(current_store).reject { |role| target.include?(role.id.to_s) }
99
+ end
100
+
92
101
  # Reconcile the user's roles on this store to match `desired_role_ids`.
93
102
  # Adds missing assignments and removes extras — no-op for unchanged.
94
103
  def apply_role_ids(desired_role_ids)
@@ -47,7 +47,7 @@ module Spree
47
47
  @resource = find_resource
48
48
  authorize!(:update, @resource)
49
49
 
50
- @resource.revoke!(try_spree_current_user)
50
+ @resource.revoke!(current_actor)
51
51
  render json: serialize_resource(@resource)
52
52
  end
53
53
 
@@ -90,7 +90,7 @@ module Spree
90
90
  # happens in `before_validation :generate_token` on the model.
91
91
  def build_resource
92
92
  scope.new(permitted_params).tap do |key|
93
- key.created_by = try_spree_current_user
93
+ key.created_by = current_actor
94
94
  end
95
95
  end
96
96
 
@@ -13,6 +13,7 @@ module Spree
13
13
 
14
14
  include Spree::Api::V3::ScopedAuthorization
15
15
  include Spree::Api::V3::Admin::ValidationDetails
16
+ include Spree::Api::V3::Admin::EmbedPermissions
16
17
  end
17
18
  end
18
19
  end
@@ -109,17 +109,22 @@ module Spree
109
109
  end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category')
110
110
  end
111
111
 
112
- # Returns the first segment whose `<segment>_id` is present in params,
112
+ # Returns the first segment whose `<segment>_id` is in the route,
113
113
  # paired with its class and the raw id value, or nil. Memoized — read
114
114
  # by both `set_parent` and `scoped_resource_name`.
115
+ #
116
+ # Read from the path only: a `store_id` or `refund_id` added to the
117
+ # query string or body must not switch the parent to a type that is
118
+ # never routed here and has no store scope to narrow it.
115
119
  def parent_lookup
116
120
  return @parent_lookup if defined?(@parent_lookup)
117
121
 
118
- match = parent_route_map.find { |segment, _| params[:"#{segment}_id"].present? }
122
+ route_params = request.path_parameters
123
+ match = parent_route_map.find { |segment, _| route_params[:"#{segment}_id"].present? }
119
124
  @parent_lookup =
120
125
  if match
121
126
  segment, klass_name = match
122
- ParentLookup.new(klass_name.constantize, params[:"#{segment}_id"], segment)
127
+ ParentLookup.new(klass_name.constantize, route_params[:"#{segment}_id"], segment)
123
128
  end
124
129
  end
125
130
  end
@@ -223,20 +223,6 @@ module Spree
223
223
  false
224
224
  end
225
225
 
226
- # Mirrors the key gate's two principals: a secret key carries scopes,
227
- # a signed-in staffer carries their roles' catalog keys.
228
- #
229
- # @param key [String]
230
- # @return [Boolean]
231
- def holds_permission?(key)
232
- return current_api_key.has_scope?(key) if scope_limited_principal?
233
-
234
- ability = current_ability
235
- return true unless ability.respond_to?(:permission_keys)
236
-
237
- ability.permission_keys.include?(key)
238
- end
239
-
240
226
  # `export` maps to `:show`; `anonymize` is a destructive write, so it
241
227
  # maps to `:destroy` rather than `:update` — a role that may correct
242
228
  # a customer's name should not thereby be able to erase their history.
@@ -13,6 +13,8 @@ module Spree
13
13
  class GiftCardsController < ResourceController
14
14
  scoped_resource :gift_cards
15
15
 
16
+ before_action :resolve_customer, only: [:create, :update]
17
+
16
18
  protected
17
19
 
18
20
  def model_class
@@ -24,8 +26,33 @@ module Spree
24
26
  end
25
27
 
26
28
  def permitted_params
27
- params.permit(*model_additional_permitted_attributes, :code, :amount, :expires_at,
28
- :customer_id, :currency)
29
+ params.permit(*model_additional_permitted_attributes, :code, :amount, :expires_at, :currency).tap do |attributes|
30
+ attributes[:customer] = @customer if params.key?(:customer_id)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Assigning a customer embeds their record in the response, so it is
37
+ # a read of customers: the caller must hold that permission, and the
38
+ # customer is resolved through their ability — never a raw id the
39
+ # model would look up across every customer.
40
+ def resolve_customer
41
+ return unless params.key?(:customer_id)
42
+ return @customer = nil if params[:customer_id].blank?
43
+
44
+ unless holds_permission?('read_customers')
45
+ return render_error(
46
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
47
+ message: 'Missing permission: read_customers',
48
+ status: :forbidden,
49
+ details: { required_permission: 'read_customers' }
50
+ )
51
+ end
52
+
53
+ @customer = Spree.customer_class.
54
+ accessible_by(current_ability, :show).
55
+ find_by_prefix_id!(params[:customer_id])
29
56
  end
30
57
  end
31
58
  end
@@ -72,7 +72,10 @@ module Spree
72
72
  end
73
73
 
74
74
  def authenticate_existing(user)
75
- return user if user.valid_password?(params[:password].to_s)
75
+ # Under the login lockout: otherwise this is an unthrottled way to
76
+ # guess an existing account's password with an invitation token.
77
+ verdict = Spree::Authentication::Lockout.check(user) { user.valid_password?(params[:password].to_s) }
78
+ return user if verdict == :valid
76
79
 
77
80
  render_error(
78
81
  code: ERROR_CODES[:authentication_failed],
@@ -0,0 +1,41 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Invitations
6
+ # The acceptance link of a pending staff invitation.
7
+ #
8
+ # The link carries the token that creates the account and grants the
9
+ # role, so it is authorized like sending the invitation: the caller
10
+ # needs write access to staff and must be allowed to grant the
11
+ # invitation's role. Reading staff is not enough.
12
+ class AcceptanceLinksController < Admin::BaseController
13
+ include Spree::Api::V3::Admin::RoleGrantGuard
14
+
15
+ scoped_resource :staff
16
+
17
+ # GET /api/v3/admin/invitations/:invitation_id/acceptance_link
18
+ def show
19
+ invitation = Spree::Invitation.
20
+ where(resource: current_store).
21
+ pending.
22
+ find_by_prefix_id!(params[:invitation_id])
23
+ authorize!(:update, invitation)
24
+ return if reject_unauthorized_role_grant!([invitation.role_id])
25
+
26
+ render json: Spree.api.admin_invitation_acceptance_link_serializer.new(
27
+ invitation, params: { store: current_store }
28
+ ).to_h
29
+ end
30
+
31
+ protected
32
+
33
+ def read_actions
34
+ []
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -58,6 +58,17 @@ module Spree
58
58
  )
59
59
  end
60
60
 
61
+ # /me describes the signed-in user rather than a store, and the
62
+ # dashboard reads it before any store is selected. A user who holds
63
+ # no role on any store (not yet given one, or removed from all of
64
+ # them) must still reach it, or they cannot finish signing in. A
65
+ # request that names a store keeps the membership check.
66
+ def require_store_membership!
67
+ return true if store_id_header.nil?
68
+
69
+ super
70
+ end
71
+
61
72
  def permitted_params
62
73
  params.permit(:selected_locale, :first_name, :last_name, :avatar)
63
74
  end
@@ -18,7 +18,7 @@ module Spree
18
18
  #
19
19
  # Editable fields only — status moves through the member actions.
20
20
  def update
21
- if @resource.update(permitted_params)
21
+ if @resource.update(update_attributes)
22
22
  render json: serialize_resource(@resource.reload)
23
23
  else
24
24
  render_validation_error(@resource.errors)
@@ -41,6 +41,19 @@ module Spree
41
41
 
42
42
  private
43
43
 
44
+ # The reason is resolved through the store rather than assigned
45
+ # raw, so another store's reads as missing.
46
+ def update_attributes
47
+ attributes = permitted_params.to_h.except('reason_id')
48
+
49
+ if params.key?(:reason_id)
50
+ attributes['reason'] =
51
+ (current_store.claim_reasons.find_by_prefix_id!(params[:reason_id]) if params[:reason_id].present?)
52
+ end
53
+
54
+ attributes
55
+ end
56
+
44
57
  # The whole store's catalogue: an operator may promise anything
45
58
  # the store sells as a replacement.
46
59
  def replacement_variant_for(variant_id)
@@ -19,7 +19,7 @@ module Spree
19
19
  #
20
20
  # Editable fields only — status moves through the member actions.
21
21
  def update
22
- if @resource.update(permitted_params)
22
+ if @resource.update(update_attributes)
23
23
  render json: serialize_resource(@resource.reload)
24
24
  else
25
25
  render_validation_error(@resource.errors)
@@ -42,6 +42,27 @@ module Spree
42
42
 
43
43
  private
44
44
 
45
+ # The ids are resolved through the store rather than assigned raw,
46
+ # so another store's reason or warehouse reads as missing.
47
+ def update_attributes
48
+ attributes = permitted_params.to_h.except('reason_id', 'stock_location_id')
49
+
50
+ if params.key?(:reason_id)
51
+ attributes['reason'] =
52
+ (current_store.return_reasons.find_by_prefix_id!(params[:reason_id]) if params[:reason_id].present?)
53
+ end
54
+
55
+ if params.key?(:stock_location_id)
56
+ attributes['stock_location'] =
57
+ if params[:stock_location_id].present?
58
+ current_store.stock_locations.accessible_by(current_ability, :show).
59
+ find_by_prefix_id!(params[:stock_location_id])
60
+ end
61
+ end
62
+
63
+ attributes
64
+ end
65
+
45
66
  # The whole store's catalogue, and any warehouse the caller may
46
67
  # see: an operator exchanges into anything the store sells.
47
68
  def replacement_variant_for(variant_id)
@@ -113,7 +113,7 @@ module Spree
113
113
  end
114
114
 
115
115
  def find_stock_location!(id)
116
- Spree::StockLocation.accessible_by(current_ability, :show).find_by_prefix_id!(id)
116
+ current_store.stock_locations.accessible_by(current_ability, :show).find_by_prefix_id!(id)
117
117
  end
118
118
 
119
119
  def stock_location_for_create