parse-stack-next 5.5.0 → 5.5.2
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 +4 -4
- data/CHANGELOG.md +190 -0
- data/Gemfile.lock +1 -1
- data/README.md +16 -3
- data/docs/atlas_vector_search_guide.md +5 -1
- data/lib/parse/acl_scope.rb +11 -0
- data/lib/parse/agent/mcp_rack_app.rb +53 -14
- data/lib/parse/agent/mcp_server.rb +19 -0
- data/lib/parse/api/path_segment.rb +31 -0
- data/lib/parse/api/users.rb +3 -0
- data/lib/parse/cache/redis.rb +55 -11
- data/lib/parse/client/body_builder.rb +71 -8
- data/lib/parse/client/caching.rb +12 -3
- data/lib/parse/client/logging.rb +9 -0
- data/lib/parse/client.rb +18 -2
- data/lib/parse/embeddings/cache.rb +60 -8
- data/lib/parse/model/core/properties.rb +42 -5
- data/lib/parse/mongodb.rb +12 -0
- data/lib/parse/pipeline_security.rb +81 -15
- data/lib/parse/query.rb +183 -58
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +12 -1
- metadata +1 -1
data/lib/parse/query.rb
CHANGED
|
@@ -1811,13 +1811,25 @@ module Parse
|
|
|
1811
1811
|
# Whether this query carries a non-master-key auth scope. Used by
|
|
1812
1812
|
# `#distinct` (and group_by aggregations) to decide whether to
|
|
1813
1813
|
# auto-promote the REST aggregate path to mongo-direct so the SDK's
|
|
1814
|
-
# ACLScope / CLPScope enforcement actually runs.
|
|
1814
|
+
# ACLScope / CLPScope enforcement actually runs. Also detects the
|
|
1815
|
+
# fiber-local ambient session set by Parse.with_session so that
|
|
1816
|
+
# aggregations inside a with_session block are treated as scoped —
|
|
1817
|
+
# consistent with how Parse::Client#request already scopes REST calls.
|
|
1815
1818
|
# @return [Boolean]
|
|
1816
1819
|
# @api private
|
|
1817
1820
|
def distinct_query_is_scoped?
|
|
1818
1821
|
return true if @session_token.is_a?(String) && !@session_token.empty?
|
|
1819
1822
|
return true if @acl_user
|
|
1820
1823
|
return true if @acl_role
|
|
1824
|
+
# An ambient Parse.with_session counts as scope ONLY when the query did
|
|
1825
|
+
# not explicitly request master-key mode — mirroring Parse::Client#request,
|
|
1826
|
+
# where an explicit use_master_key: true is a deliberate admin call that
|
|
1827
|
+
# skips the ambient session. Otherwise an admin aggregation inside a
|
|
1828
|
+
# with_session block would be wrongly forced to mongo-direct / fail-closed.
|
|
1829
|
+
unless use_master_key == true
|
|
1830
|
+
ambient = ambient_session_token
|
|
1831
|
+
return true if ambient.is_a?(String) && !ambient.empty?
|
|
1832
|
+
end
|
|
1821
1833
|
false
|
|
1822
1834
|
end
|
|
1823
1835
|
|
|
@@ -1832,12 +1844,13 @@ module Parse
|
|
|
1832
1844
|
def raise_scoped_aggregation_requires_mongo_direct!
|
|
1833
1845
|
raise MongoDirectRequired,
|
|
1834
1846
|
"[Parse::Query] This scoped aggregation (session_token / " \
|
|
1835
|
-
"scope_to_user / scope_to_role
|
|
1836
|
-
"SDK can enforce ACL/CLP.
|
|
1837
|
-
"endpoint is master-key-only and
|
|
1838
|
-
"it there would silently run unscoped
|
|
1839
|
-
"Enable mongo-direct via
|
|
1840
|
-
"rewrite without the
|
|
1847
|
+
"scope_to_user / scope_to_role, or an active Parse.with_session " \
|
|
1848
|
+
"block) requires mongo-direct so the SDK can enforce ACL/CLP. " \
|
|
1849
|
+
"Parse Server's REST /aggregate endpoint is master-key-only and " \
|
|
1850
|
+
"enforces neither, so routing it there would silently run unscoped " \
|
|
1851
|
+
"as the master key. Enable mongo-direct via " \
|
|
1852
|
+
"Parse::MongoDB.configure(...), or rewrite without the " \
|
|
1853
|
+
"aggregation terminal."
|
|
1841
1854
|
end
|
|
1842
1855
|
|
|
1843
1856
|
# Scope a query to a specific user's row-level ACL when it auto-routes
|
|
@@ -1965,11 +1978,21 @@ module Parse
|
|
|
1965
1978
|
# roles, injects the three-layer ACL simulation
|
|
1966
1979
|
# (top-level `$match`, `$lookup` rewriter, post-fetch
|
|
1967
1980
|
# redactor) via {Parse::MongoDB.aggregate}.
|
|
1981
|
+
# * an active `Parse.with_session` block — the fiber-local ambient
|
|
1982
|
+
# session token scopes the read the same way an explicit
|
|
1983
|
+
# `session_token=` would (see {#mongo_direct_auth_kwargs}).
|
|
1968
1984
|
#
|
|
1969
1985
|
# Raises a clear {MongoDirectRequired} otherwise.
|
|
1970
1986
|
# @!visibility private
|
|
1971
1987
|
def assert_mongo_direct_routable!
|
|
1972
1988
|
has_session = @session_token.is_a?(String) && !@session_token.empty?
|
|
1989
|
+
# An active `Parse.with_session` block scopes the read even on a
|
|
1990
|
+
# non-master client (client_mode, or a user-scoped client with no
|
|
1991
|
+
# master key), where `server_mode_master` is false. Without this the
|
|
1992
|
+
# query would raise instead of running scoped — and on a master
|
|
1993
|
+
# client the ambient is what `mongo_direct_auth_kwargs` forwards so
|
|
1994
|
+
# the read is scoped rather than silently master.
|
|
1995
|
+
has_ambient_session = !ambient_session_token.nil?
|
|
1973
1996
|
# Mirror the request-layer auth resolution in Parse::Client#request:
|
|
1974
1997
|
# when the process is in "server mode" — Parse.client_mode == false
|
|
1975
1998
|
# AND the resolved Parse::Client has a master_key — and the caller
|
|
@@ -1985,7 +2008,7 @@ module Parse
|
|
|
1985
2008
|
false
|
|
1986
2009
|
end
|
|
1987
2010
|
server_mode_master = (use_master_key != false) && !Parse.client_mode && client_has_master_key
|
|
1988
|
-
unless use_master_key || server_mode_master || @acl_user || @acl_role || has_session
|
|
2011
|
+
unless use_master_key || server_mode_master || @acl_user || @acl_role || has_session || has_ambient_session
|
|
1989
2012
|
raise MongoDirectRequired,
|
|
1990
2013
|
"[Parse::Query] This query uses a constraint that can only run " \
|
|
1991
2014
|
"via mongo-direct. Mongo-direct bypasses Parse Server's enforcement, " \
|
|
@@ -2019,6 +2042,12 @@ module Parse
|
|
|
2019
2042
|
# double-inject).
|
|
2020
2043
|
# * `session_token` is set → forward `session_token:` so
|
|
2021
2044
|
# Parse::ACLScope runs the full three-layer simulation.
|
|
2045
|
+
# * Otherwise, the fiber-local ambient session set by
|
|
2046
|
+
# `Parse.with_session` is forwarded as `session_token:` (unless
|
|
2047
|
+
# the caller explicitly requested `use_master_key: true`), so a
|
|
2048
|
+
# query that auto-routes to mongo-direct inside a `with_session`
|
|
2049
|
+
# block is scoped to that user — matching what the REST path does
|
|
2050
|
+
# in {Parse::Client#request}.
|
|
2022
2051
|
# * Otherwise (master-key path) → forward `master: true`.
|
|
2023
2052
|
# @!visibility private
|
|
2024
2053
|
def mongo_direct_auth_kwargs
|
|
@@ -2036,11 +2065,32 @@ module Parse
|
|
|
2036
2065
|
{ acl_role: @acl_role }
|
|
2037
2066
|
elsif @session_token.is_a?(String) && !@session_token.empty?
|
|
2038
2067
|
{ session_token: @session_token }
|
|
2068
|
+
elsif use_master_key != true && (ambient = ambient_session_token)
|
|
2069
|
+
# No explicit per-query scope, but a `Parse.with_session` block is
|
|
2070
|
+
# active. Mirror Parse::Client#request's precedence (ambient
|
|
2071
|
+
# session wins over the server-mode master default) so the read is
|
|
2072
|
+
# scoped to that user instead of silently running as master with
|
|
2073
|
+
# no ACL/CLP enforcement. An explicit `use_master_key: true` is a
|
|
2074
|
+
# deliberate admin call and skips the ambient, exactly as the REST
|
|
2075
|
+
# path does.
|
|
2076
|
+
{ session_token: ambient }
|
|
2039
2077
|
else
|
|
2040
2078
|
{ master: true }
|
|
2041
2079
|
end
|
|
2042
2080
|
end
|
|
2043
2081
|
|
|
2082
|
+
# The fiber-local ambient session token set by `Parse.with_session`,
|
|
2083
|
+
# or nil. A whitespace-only ambient is treated as absent so it cannot
|
|
2084
|
+
# block the master fallback and then fail a later presence check —
|
|
2085
|
+
# the same guard {Parse::Client#request} applies.
|
|
2086
|
+
# @return [String, nil]
|
|
2087
|
+
# @!visibility private
|
|
2088
|
+
def ambient_session_token
|
|
2089
|
+
return nil unless Parse.respond_to?(:current_session_token)
|
|
2090
|
+
ambient = Parse.current_session_token
|
|
2091
|
+
ambient if ambient.is_a?(String) && !ambient.strip.empty?
|
|
2092
|
+
end
|
|
2093
|
+
|
|
2044
2094
|
# Check if this query contains constraints that require aggregation pipeline processing
|
|
2045
2095
|
# @return [Boolean] true if aggregation pipeline is required
|
|
2046
2096
|
def requires_aggregation_pipeline?
|
|
@@ -3527,22 +3577,31 @@ module Parse
|
|
|
3527
3577
|
# the merged pipeline is provably SDK-injected, never user input.
|
|
3528
3578
|
uses_internal_fields = pipeline_uses_internal_fields?(complete_pipeline)
|
|
3529
3579
|
scoped = distinct_query_is_scoped?
|
|
3580
|
+
mongo_ready = defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
|
|
3530
3581
|
use_mongo_direct = mongo_direct
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3582
|
+
|
|
3583
|
+
if scoped
|
|
3584
|
+
# A scoped aggregation (session_token / scope_to_user / scope_to_role)
|
|
3585
|
+
# must NEVER reach Parse Server's REST /aggregate endpoint — it is
|
|
3586
|
+
# master-key-only and enforces NEITHER ACL NOR CLP, so it would run
|
|
3587
|
+
# unscoped as the master key. This holds even when the caller
|
|
3588
|
+
# explicitly passes `mongo_direct: false`: an explicit false cannot
|
|
3589
|
+
# opt a scoped query out of ACL/CLP enforcement. Promote to mongo-
|
|
3590
|
+
# direct, or fail closed when direct Mongo is unavailable (refuse
|
|
3591
|
+
# rather than leak unscoped rows).
|
|
3592
|
+
if mongo_ready
|
|
3593
|
+
use_mongo_direct = true
|
|
3594
|
+
else
|
|
3595
|
+
raise_scoped_aggregation_requires_mongo_direct!
|
|
3596
|
+
end
|
|
3597
|
+
elsif use_mongo_direct.nil?
|
|
3598
|
+
# Unscoped auto-routing: $inQuery/$notInQuery → $lookup pipelines and
|
|
3599
|
+
# SDK-injected internal-field ($rperm/_wperm) pipelines can't be served
|
|
3600
|
+
# by REST /aggregate, so prefer mongo-direct when available. An unscoped
|
|
3601
|
+
# internal-field pipeline keeps the REST fallback (a master-key
|
|
3602
|
+
# correctness edge, not an enforcement bypass).
|
|
3603
|
+
if (lookup_stages && lookup_stages.any?) || uses_internal_fields
|
|
3534
3604
|
use_mongo_direct = true if mongo_ready
|
|
3535
|
-
elsif scoped || uses_internal_fields
|
|
3536
|
-
if mongo_ready
|
|
3537
|
-
use_mongo_direct = true
|
|
3538
|
-
elsif scoped
|
|
3539
|
-
# Fail closed: a scoped aggregation cannot fall back to REST
|
|
3540
|
-
# /aggregate without silently bypassing ACL/CLP (master-key-only
|
|
3541
|
-
# endpoint). Refuse rather than leak unscoped results. Unscoped
|
|
3542
|
-
# internal-field pipelines keep the REST fallback (a master-key
|
|
3543
|
-
# correctness edge, not an enforcement bypass).
|
|
3544
|
-
raise_scoped_aggregation_requires_mongo_direct!
|
|
3545
|
-
end
|
|
3546
3605
|
end
|
|
3547
3606
|
end
|
|
3548
3607
|
|
|
@@ -3641,17 +3700,21 @@ module Parse
|
|
|
3641
3700
|
# unenforced). A scoped query fails closed when mongo-direct is
|
|
3642
3701
|
# unavailable rather than silently running unscoped as master.
|
|
3643
3702
|
scoped = distinct_query_is_scoped?
|
|
3703
|
+
mongo_ready = defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
|
|
3644
3704
|
use_mongo_direct = mongo_direct
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3705
|
+
|
|
3706
|
+
if scoped
|
|
3707
|
+
# A scoped aggregation must never reach REST /aggregate (master-key-
|
|
3708
|
+
# only, unenforced) — not even when the caller explicitly passes
|
|
3709
|
+
# mongo_direct: false. Promote to mongo-direct, or fail closed.
|
|
3710
|
+
if mongo_ready
|
|
3711
|
+
use_mongo_direct = true
|
|
3712
|
+
else
|
|
3713
|
+
raise_scoped_aggregation_requires_mongo_direct!
|
|
3714
|
+
end
|
|
3715
|
+
elsif use_mongo_direct.nil?
|
|
3716
|
+
if has_lookup_stages || uses_internal_fields
|
|
3648
3717
|
use_mongo_direct = true if mongo_ready
|
|
3649
|
-
elsif scoped || uses_internal_fields
|
|
3650
|
-
if mongo_ready
|
|
3651
|
-
use_mongo_direct = true
|
|
3652
|
-
elsif scoped
|
|
3653
|
-
raise_scoped_aggregation_requires_mongo_direct!
|
|
3654
|
-
end
|
|
3655
3718
|
end
|
|
3656
3719
|
end
|
|
3657
3720
|
|
|
@@ -5946,13 +6009,21 @@ module Parse
|
|
|
5946
6009
|
if @mongo_direct && defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
|
|
5947
6010
|
@cached_response = execute_direct!
|
|
5948
6011
|
else
|
|
6012
|
+
# REST /aggregate is master-key-only. An ambient Parse.with_session
|
|
6013
|
+
# block would suppress the master key via session_token, causing a
|
|
6014
|
+
# 401/403. Force use_master_key unless the caller explicitly disabled
|
|
6015
|
+
# it (use_master_key: false is a deliberate client-mode decision).
|
|
6016
|
+
# `.dup` keeps the master-key flip local to this call even if `_opts`
|
|
6017
|
+
# ever returns a shared/memoized hash.
|
|
6018
|
+
rest_opts = @query.send(:_opts).dup
|
|
6019
|
+
rest_opts[:use_master_key] = true unless rest_opts[:use_master_key] == false
|
|
5949
6020
|
@cached_response = @query.client.aggregate_pipeline(
|
|
5950
6021
|
@query.instance_variable_get(:@table),
|
|
5951
6022
|
@pipeline,
|
|
5952
6023
|
headers: {},
|
|
5953
6024
|
raw_values: @raw_values,
|
|
5954
6025
|
raw_field_names: @raw_field_names,
|
|
5955
|
-
|
|
6026
|
+
**rest_opts,
|
|
5956
6027
|
)
|
|
5957
6028
|
end
|
|
5958
6029
|
|
|
@@ -6338,16 +6409,24 @@ module Parse
|
|
|
6338
6409
|
# @return [Array<Hash>] raw aggregation results
|
|
6339
6410
|
def raw(operation, aggregation_expr)
|
|
6340
6411
|
formatted_group_field = @query.send(:format_aggregation_field, @group_field)
|
|
6341
|
-
pipeline = build_pipeline(formatted_group_field, aggregation_expr)
|
|
6342
6412
|
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6413
|
+
# Build the same pipeline the count/sum/etc. terminals use, then delegate
|
|
6414
|
+
# to Query#aggregate. That central path handles scoped-query routing
|
|
6415
|
+
# (session_token / acl_user / acl_role / ambient Parse.with_session →
|
|
6416
|
+
# auto-promote to mongo-direct, or fail closed when unavailable) so a
|
|
6417
|
+
# scoped `raw` is never sent to the master-key-only REST /aggregate
|
|
6418
|
+
# endpoint, and it returns the raw Array<Hash> rows this method documents.
|
|
6419
|
+
# `$match` from the query's where constraints is added by Query#aggregate.
|
|
6420
|
+
pipeline = []
|
|
6421
|
+
pipeline << { "$unwind" => "$#{formatted_group_field}" } if @flatten_arrays
|
|
6422
|
+
pipeline << { "$group" => { "_id" => "$#{formatted_group_field}", "count" => aggregation_expr } }
|
|
6423
|
+
add_fields = size_addfields_stage
|
|
6424
|
+
pipeline << add_fields if add_fields
|
|
6425
|
+
sort = sort_stage
|
|
6426
|
+
pipeline << sort if sort
|
|
6427
|
+
pipeline << { "$project" => { "_id" => 0, "objectId" => "$_id", "count" => 1 } }
|
|
6349
6428
|
|
|
6350
|
-
|
|
6429
|
+
@query.aggregate(pipeline, verbose: @query.instance_variable_get(:@verbose_aggregate)).raw || []
|
|
6351
6430
|
end
|
|
6352
6431
|
|
|
6353
6432
|
# Count the number of items in each group.
|
|
@@ -6491,8 +6570,12 @@ module Parse
|
|
|
6491
6570
|
# already does this auto-promotion (lib/parse/agent/tools.rb), this
|
|
6492
6571
|
# is the equivalent at the Query layer for direct SDK callers.
|
|
6493
6572
|
use_mongo_direct = @mongo_direct
|
|
6494
|
-
if !use_mongo_direct && query_is_scoped?
|
|
6495
|
-
|
|
6573
|
+
if !use_mongo_direct && query_is_scoped?
|
|
6574
|
+
if parse_mongodb_available?
|
|
6575
|
+
use_mongo_direct = true
|
|
6576
|
+
else
|
|
6577
|
+
@query.send(:raise_scoped_aggregation_requires_mongo_direct!)
|
|
6578
|
+
end
|
|
6496
6579
|
end
|
|
6497
6580
|
|
|
6498
6581
|
if use_mongo_direct
|
|
@@ -6729,15 +6812,22 @@ module Parse
|
|
|
6729
6812
|
end
|
|
6730
6813
|
|
|
6731
6814
|
# Whether the parent query carries any non-master-key auth scope. A
|
|
6732
|
-
# session_token, acl_user,
|
|
6733
|
-
#
|
|
6734
|
-
# mongo-direct path. Used to decide whether to
|
|
6735
|
-
# aggregation path to mongo-direct.
|
|
6815
|
+
# session_token, acl_user, acl_role, or an active Parse.with_session
|
|
6816
|
+
# ambient means the caller expects ACL-filtered results — which only
|
|
6817
|
+
# the SDK's mongo-direct path provides. Used to decide whether to
|
|
6818
|
+
# auto-promote the REST aggregation path to mongo-direct.
|
|
6736
6819
|
def query_is_scoped?
|
|
6737
6820
|
st = @query.session_token
|
|
6738
6821
|
return true if st.is_a?(String) && !st.empty?
|
|
6739
6822
|
return true if @query.instance_variable_get(:@acl_user)
|
|
6740
6823
|
return true if @query.instance_variable_get(:@acl_role)
|
|
6824
|
+
# Ambient Parse.with_session counts as scope only when the query did not
|
|
6825
|
+
# explicitly set use_master_key: true (matches Parse::Client#request
|
|
6826
|
+
# precedence — an explicit master-key call skips the ambient session).
|
|
6827
|
+
unless @query.use_master_key == true
|
|
6828
|
+
ambient = @query.send(:ambient_session_token)
|
|
6829
|
+
return true if ambient.is_a?(String) && !ambient.empty?
|
|
6830
|
+
end
|
|
6741
6831
|
false
|
|
6742
6832
|
end
|
|
6743
6833
|
|
|
@@ -7178,14 +7268,19 @@ module Parse
|
|
|
7178
7268
|
# Format the date field name
|
|
7179
7269
|
formatted_date_field = @query.send(:format_aggregation_field, @date_field)
|
|
7180
7270
|
|
|
7181
|
-
# Auto-promote scoped queries to mongo-direct.
|
|
7182
|
-
#
|
|
7183
|
-
#
|
|
7184
|
-
#
|
|
7185
|
-
# mongo-direct
|
|
7271
|
+
# Auto-promote scoped queries to mongo-direct. REST `/aggregate` is
|
|
7272
|
+
# master-key-only and enforces neither ACL nor CLP — a scoped query
|
|
7273
|
+
# (session_token / acl_user / acl_role, or an active
|
|
7274
|
+
# Parse.with_session block) must use the SDK's enforcement layers.
|
|
7275
|
+
# Fail closed if mongo-direct is unavailable rather than silently
|
|
7276
|
+
# returning unscoped rows. Mirrors the scoped-query gate in Query#aggregate.
|
|
7186
7277
|
use_mongo_direct = @mongo_direct
|
|
7187
|
-
if !use_mongo_direct && query_is_scoped?
|
|
7188
|
-
|
|
7278
|
+
if !use_mongo_direct && query_is_scoped?
|
|
7279
|
+
if parse_mongodb_available?
|
|
7280
|
+
use_mongo_direct = true
|
|
7281
|
+
else
|
|
7282
|
+
@query.send(:raise_scoped_aggregation_requires_mongo_direct!)
|
|
7283
|
+
end
|
|
7189
7284
|
end
|
|
7190
7285
|
|
|
7191
7286
|
if use_mongo_direct
|
|
@@ -7231,11 +7326,21 @@ module Parse
|
|
|
7231
7326
|
puts "[VERBOSE AGGREGATE] Sending to: #{@query.instance_variable_get(:@table)}"
|
|
7232
7327
|
end
|
|
7233
7328
|
|
|
7329
|
+
# Parse Server's REST /aggregate endpoint is master-key-only. An active
|
|
7330
|
+
# Parse.with_session block sets a fiber-local ambient session token that
|
|
7331
|
+
# Parse::Client#request picks up and uses in place of the master key,
|
|
7332
|
+
# causing a 401/403 on this endpoint. Force use_master_key: true so the
|
|
7333
|
+
# ambient session cannot suppress it — unless the caller explicitly set
|
|
7334
|
+
# use_master_key: false (deliberate client-mode / session-token intent).
|
|
7335
|
+
# `.dup` keeps the master-key flip local to this call (see Aggregation#execute!).
|
|
7336
|
+
rest_opts = @query.send(:_opts).dup
|
|
7337
|
+
rest_opts[:use_master_key] = true unless rest_opts[:use_master_key] == false
|
|
7338
|
+
|
|
7234
7339
|
response = @query.client.aggregate_pipeline(
|
|
7235
7340
|
@query.instance_variable_get(:@table),
|
|
7236
7341
|
pipeline,
|
|
7237
7342
|
headers: {},
|
|
7238
|
-
|
|
7343
|
+
**rest_opts,
|
|
7239
7344
|
)
|
|
7240
7345
|
|
|
7241
7346
|
if @query.instance_variable_get(:@verbose_aggregate)
|
|
@@ -7262,6 +7367,18 @@ module Parse
|
|
|
7262
7367
|
end
|
|
7263
7368
|
result_hash
|
|
7264
7369
|
else
|
|
7370
|
+
unless response.success?
|
|
7371
|
+
# Surface the failure (the result would otherwise be a silent `{}`)
|
|
7372
|
+
# through the configured logger rather than unconditional stderr.
|
|
7373
|
+
# Log the error code + message, not a full `inspect`, to avoid
|
|
7374
|
+
# echoing an unbounded server payload into logs.
|
|
7375
|
+
logger = Parse.respond_to?(:logger) ? Parse.logger : nil
|
|
7376
|
+
logger&.warn(
|
|
7377
|
+
"[Parse::GroupByDate] aggregate failed " \
|
|
7378
|
+
"(#{@query.instance_variable_get(:@table)} :#{@date_field} :#{@interval}): " \
|
|
7379
|
+
"code=#{response.code} #{response.error}"
|
|
7380
|
+
)
|
|
7381
|
+
end
|
|
7265
7382
|
{}
|
|
7266
7383
|
end
|
|
7267
7384
|
end
|
|
@@ -7442,15 +7559,23 @@ module Parse
|
|
|
7442
7559
|
{ "$sort" => { field => dir } }
|
|
7443
7560
|
end
|
|
7444
7561
|
|
|
7445
|
-
# Mirror of {GroupBy#query_is_scoped?}. A session_token, acl_user,
|
|
7446
|
-
# acl_role
|
|
7447
|
-
# which only the mongo-direct path
|
|
7448
|
-
# `/aggregate` is master-key-only and
|
|
7562
|
+
# Mirror of {GroupBy#query_is_scoped?}. A session_token, acl_user,
|
|
7563
|
+
# acl_role, or an active Parse.with_session ambient means the caller
|
|
7564
|
+
# expects ACL-filtered results — which only the mongo-direct path
|
|
7565
|
+
# provides. Parse Server REST `/aggregate` is master-key-only and
|
|
7566
|
+
# unscoped.
|
|
7449
7567
|
def query_is_scoped?
|
|
7450
7568
|
st = @query.session_token
|
|
7451
7569
|
return true if st.is_a?(String) && !st.empty?
|
|
7452
7570
|
return true if @query.instance_variable_get(:@acl_user)
|
|
7453
7571
|
return true if @query.instance_variable_get(:@acl_role)
|
|
7572
|
+
# Ambient Parse.with_session counts as scope only when the query did not
|
|
7573
|
+
# explicitly set use_master_key: true (matches Parse::Client#request
|
|
7574
|
+
# precedence — an explicit master-key call skips the ambient session).
|
|
7575
|
+
unless @query.use_master_key == true
|
|
7576
|
+
ambient = @query.send(:ambient_session_token)
|
|
7577
|
+
return true if ambient.is_a?(String) && !ambient.empty?
|
|
7578
|
+
end
|
|
7454
7579
|
false
|
|
7455
7580
|
end
|
|
7456
7581
|
|
data/lib/parse/stack/version.rb
CHANGED
data/lib/parse/stack.rb
CHANGED
|
@@ -582,8 +582,19 @@ module Parse
|
|
|
582
582
|
|
|
583
583
|
# Optional dedicated Moneta store for the synchronize-create lock. When
|
|
584
584
|
# nil, falls back to {Parse.cache}.
|
|
585
|
+
#
|
|
586
|
+
# SECURITY: if you pass a raw Moneta-Redis store, build it with
|
|
587
|
+
# +value_serializer: nil+. The lock release path reads the stored owner
|
|
588
|
+
# token back (+store[key]+) to compare-and-delete; with Moneta's default
|
|
589
|
+
# Marshal value serializer that read +Marshal.load+s bytes from Redis — an
|
|
590
|
+
# RCE vector on a shared/untrusted/MITM'd lock store. With
|
|
591
|
+
# +value_serializer: nil+ the owner token is a plain string and is never
|
|
592
|
+
# deserialized. Alternatively pass a {Parse::Cache::Redis} instance, which
|
|
593
|
+
# uses a raw-string acquire/release path and avoids Marshal entirely.
|
|
585
594
|
# @example
|
|
586
|
-
# Parse.synchronize_create_store = Moneta.new(:Redis, url: "redis://locks:6379/1")
|
|
595
|
+
# Parse.synchronize_create_store = Moneta.new(:Redis, url: "redis://locks:6379/1", value_serializer: nil)
|
|
596
|
+
# # or, preferred:
|
|
597
|
+
# Parse.synchronize_create_store = Parse::Cache::Redis.new(url: "redis://locks:6379/1")
|
|
587
598
|
@synchronize_create_store = nil
|
|
588
599
|
|
|
589
600
|
# Optional allowlist of {Parse::Object} subclasses that may use the
|