parse-stack-next 5.5.4 → 5.5.5
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 +8 -6
- data/README.md +26 -13
- data/bin/parse-console +9 -1
- data/docs/TEST_SERVER.md +115 -238
- data/docs/mcp_guide.md +1 -1
- data/docs/mongodb_index_optimization_guide.md +3 -2
- data/docs/usage_guide.md +1 -1
- data/docs/yard-template/default/fulldoc/html/css/common.css +52 -9
- data/docs/yard-template/default/fulldoc/html/css/full_list.css +40 -13
- data/lib/parse/agent/constraint_translator.rb +18 -18
- data/lib/parse/agent/errors.rb +29 -7
- data/lib/parse/agent/metadata_dsl.rb +6 -6
- data/lib/parse/agent/tools.rb +250 -59
- data/lib/parse/agent.rb +42 -30
- data/lib/parse/api/aggregate.rb +3 -3
- data/lib/parse/api/cloud_functions.rb +19 -10
- data/lib/parse/api/objects.rb +8 -8
- data/lib/parse/api/users.rb +9 -9
- data/lib/parse/atlas_search/session.rb +34 -34
- data/lib/parse/atlas_search.rb +243 -110
- data/lib/parse/client/body_builder.rb +10 -10
- data/lib/parse/client/logging.rb +5 -2
- data/lib/parse/client/profiling.rb +5 -2
- data/lib/parse/client/protocol.rb +1 -1
- data/lib/parse/client/url_redaction.rb +94 -0
- data/lib/parse/client.rb +43 -28
- data/lib/parse/embeddings/image_fetch.rb +6 -1
- data/lib/parse/embeddings/voyage.rb +16 -17
- data/lib/parse/live_query/client.rb +7 -7
- data/lib/parse/live_query/subscription.rb +1 -1
- data/lib/parse/lock.rb +1 -1
- data/lib/parse/lock_backend.rb +118 -2
- data/lib/parse/model/acl.rb +24 -24
- data/lib/parse/model/classes/job_schedule.rb +8 -8
- data/lib/parse/model/classes/job_status.rb +9 -9
- data/lib/parse/model/classes/role.rb +49 -49
- data/lib/parse/model/classes/session.rb +2 -2
- data/lib/parse/model/classes/user.rb +66 -66
- data/lib/parse/model/core/builder.rb +7 -7
- data/lib/parse/model/core/create_lock.rb +1 -1
- data/lib/parse/model/core/properties.rb +4 -4
- data/lib/parse/model/file.rb +57 -16
- data/lib/parse/model/model.rb +19 -19
- data/lib/parse/model/object.rb +38 -38
- data/lib/parse/model/pointer.rb +4 -4
- data/lib/parse/model/push.rb +5 -5
- data/lib/parse/mongodb.rb +84 -26
- data/lib/parse/pipeline_security.rb +2 -2
- data/lib/parse/query/constraints.rb +38 -38
- data/lib/parse/query.rb +151 -75
- data/lib/parse/retrieval/reranker/cohere.rb +30 -0
- data/lib/parse/schema.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +23 -10
- data/lib/parse/two_factor_auth/user_extension.rb +25 -25
- data/lib/parse/webhooks/payload.rb +35 -35
- data/lib/parse/webhooks/registration.rb +2 -2
- data/lib/parse/webhooks/replay_protection.rb +16 -16
- data/lib/parse/webhooks.rb +11 -11
- data/parse-stack-next.gemspec +19 -1
- metadata +2 -38
- data/.bundle/config +0 -5
- data/.env.sample +0 -138
- data/.env.test +0 -10
- data/.github/ISSUE_TEMPLATE/bug_report.yml +0 -105
- data/.github/ISSUE_TEMPLATE/feature_request.yml +0 -67
- data/.github/dependabot.yml +0 -13
- data/.github/workflows/codeql.yml +0 -44
- data/.github/workflows/docs.yml +0 -39
- data/.github/workflows/release.yml +0 -43
- data/.github/workflows/ruby.yml +0 -38
- data/.gitignore +0 -56
- data/.ruby-version +0 -1
- data/.solargraph.yml +0 -22
- data/.vscode/settings.json +0 -3
- data/.yardopts +0 -19
- data/Gemfile +0 -43
- data/Gemfile.lock +0 -198
- data/Makefile +0 -63
- data/Rakefile +0 -825
- data/config/parse-config.json +0 -12
- data/scripts/debug-ips.js +0 -35
- data/scripts/docker/Dockerfile.parse +0 -17
- data/scripts/docker/atlas-init.js +0 -284
- data/scripts/docker/docker-compose.atlas.yml +0 -80
- data/scripts/docker/docker-compose.test.yml +0 -159
- data/scripts/docker/docker-compose.verifyemail.yml +0 -4
- data/scripts/docker/mongo-init.js +0 -21
- data/scripts/docker/preflight.sh +0 -76
- data/scripts/eval_mcp_with_lm_studio.rb +0 -274
- data/scripts/start-parse.sh +0 -154
- data/scripts/start_mcp_server.rb +0 -78
- data/scripts/test_server_connection.rb +0 -82
- data/scripts/vector_prototype/create_vector_index.js +0 -105
- data/scripts/vector_prototype/fetch_embeddings.py +0 -241
- data/scripts/vector_prototype/fixture_manifest.json +0 -9
- data/scripts/vector_prototype/query_prototype.rb +0 -84
- data/scripts/vector_prototype/run.sh +0 -34
data/lib/parse/query.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Parse
|
|
|
19
19
|
# your Parse collections by utilizing the {http://docs.parseplatform.org/rest/guide/#queries
|
|
20
20
|
# REST Querying interface}. This is the main engine behind making Parse queries
|
|
21
21
|
# on remote collections. It takes a set of constraints and generates the
|
|
22
|
-
# proper hash parameters that are passed to an API request in order to
|
|
22
|
+
# proper hash parameters that are passed to an API request in order to retrieve
|
|
23
23
|
# matching results. The querying design pattern is inspired from
|
|
24
24
|
# {http://datamapper.org/ DataMapper} where symbols are overloaded with
|
|
25
25
|
# specific methods with attached values.
|
|
@@ -44,19 +44,19 @@ module Parse
|
|
|
44
44
|
# names when compiling the query. This feature can be overridden by changing the
|
|
45
45
|
# value of {Parse::Query.field_formatter}.
|
|
46
46
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
47
|
+
# # default uses :columnize
|
|
48
|
+
# query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3
|
|
49
|
+
# query.compile_where # => {"fieldOne"=>1, "fieldTwo"=>2, "fieldThree"=>3}
|
|
50
50
|
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
51
|
+
# # turn off
|
|
52
|
+
# Parse::Query.field_formatter = nil
|
|
53
|
+
# query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3
|
|
54
|
+
# query.compile_where # => {"field_one"=>1, "FieldTwo"=>2, "Field_Three"=>3}
|
|
55
55
|
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
56
|
+
# # force everything camel case
|
|
57
|
+
# Parse::Query.field_formatter = :camelize
|
|
58
|
+
# query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3
|
|
59
|
+
# query.compile_where # => {"FieldOne"=>1, "FieldTwo"=>2, "FieldThree"=>3}
|
|
60
60
|
#
|
|
61
61
|
# Most of the constraints supported by Parse are available to `Parse::Query`.
|
|
62
62
|
# Assuming you have a column named `field`, here are some examples. For an
|
|
@@ -524,6 +524,25 @@ module Parse
|
|
|
524
524
|
# Normalize to symbol for comparison (handles both string and symbol keys)
|
|
525
525
|
expr_sym = expression.respond_to?(:to_sym) ? expression.to_sym : expression
|
|
526
526
|
|
|
527
|
+
# SEC-06: privilege/auth control options must NOT be settable from an
|
|
528
|
+
# untrusted, string-keyed conditions hash. `use_master_key` via a
|
|
529
|
+
# forwarded params hash (e.g. `Query.new(Cls, {"use_master_key" =>
|
|
530
|
+
# true, "ownerId" => "victim"})`) is an ACL/CLP-bypass mass-
|
|
531
|
+
# assignment; `session` swaps the query's auth principal. Honor these
|
|
532
|
+
# two ONLY when the caller passed a Symbol key (code-authored). A
|
|
533
|
+
# string form is treated as an ordinary field constraint — fail-safe:
|
|
534
|
+
# it matches a literal field of that name and never elevates.
|
|
535
|
+
if !expression.is_a?(Symbol) && (expr_sym == :use_master_key || expr_sym == :session)
|
|
536
|
+
warn "[Parse::Query] Ignoring string-keyed control option " \
|
|
537
|
+
"#{expression.inspect}; auth/privilege options (:use_master_key, " \
|
|
538
|
+
":session) are honored only as symbol keys to prevent " \
|
|
539
|
+
"mass-assignment from untrusted params. Treating it as a field " \
|
|
540
|
+
"constraint. Set it via the query method (e.g. `use_master_key: " \
|
|
541
|
+
"true` as a symbol) if you intended to control authentication."
|
|
542
|
+
add_constraint(expression, value)
|
|
543
|
+
next
|
|
544
|
+
end
|
|
545
|
+
|
|
527
546
|
if expr_sym == :order
|
|
528
547
|
order value
|
|
529
548
|
elsif expr_sym == :keys
|
|
@@ -635,23 +654,23 @@ module Parse
|
|
|
635
654
|
# When set, Parse Server excludes the named fields from each returned
|
|
636
655
|
# object, complementing the {#keys} allowlist. The two options can be
|
|
637
656
|
# combined: Parse Server first applies the {#keys} allowlist, then
|
|
638
|
-
# strips any field names listed in
|
|
657
|
+
# strips any field names listed in `excludeKeys`.
|
|
639
658
|
#
|
|
640
|
-
# @note On the REST query path (
|
|
641
|
-
# Parse Server's path-scoped
|
|
642
|
-
# (explicit
|
|
643
|
-
# auto-promotes — e.g. an
|
|
644
|
-
# a
|
|
659
|
+
# @note On the REST query path (`encode: true` in {#compile}) this maps to
|
|
660
|
+
# Parse Server's path-scoped `excludeKeys`. On the mongo-direct path
|
|
661
|
+
# (explicit `.results_direct`, an auto-route, or an aggregation that
|
|
662
|
+
# auto-promotes — e.g. an `$inQuery` pointer constraint that rewrites to
|
|
663
|
+
# a `$lookup`) the pipeline can only project the {#keys} allowlist, so
|
|
645
664
|
# the SDK honors the denylist as a post-fetch sanitize over the returned
|
|
646
665
|
# results instead. That mongo-direct sanitize is recursive by name: it
|
|
647
666
|
# strips EVERY key with a matching name at any depth, so excluding a
|
|
648
667
|
# field also removes a same-named field inside included/nested objects —
|
|
649
668
|
# broader than the REST path's top-level/dotted scoping. Reserved
|
|
650
|
-
# envelope fields (
|
|
651
|
-
#
|
|
669
|
+
# envelope fields (`objectId`, `className`, `__type`, `createdAt`,
|
|
670
|
+
# `updatedAt`, `ACL` and their Mongo storage-form names) are never
|
|
652
671
|
# stripped, so object reconstruction is unaffected. The raw aggregation
|
|
653
672
|
# accessor (`aggregate(...).raw`) returns unredacted documents — the
|
|
654
|
-
# sanitize applies to the object/decoded result paths.
|
|
673
|
+
# sanitize applies to the object/decoded result paths. `excludeKeys` is a
|
|
655
674
|
# projection convenience, not an ACL/CLP boundary, so it does not affect
|
|
656
675
|
# access control.
|
|
657
676
|
#
|
|
@@ -731,7 +750,7 @@ module Parse
|
|
|
731
750
|
# Song.all :order => { :like_count => :desc, :name => :asc }
|
|
732
751
|
# @param ordering [Parse::Order, Symbol, String, Hash] one or more
|
|
733
752
|
# ordering directives. A Hash maps field => direction. Unsupported
|
|
734
|
-
# argument types raise
|
|
753
|
+
# argument types raise `ArgumentError` rather than being silently
|
|
735
754
|
# dropped.
|
|
736
755
|
# @return [self]
|
|
737
756
|
def order(*ordering)
|
|
@@ -808,9 +827,9 @@ module Parse
|
|
|
808
827
|
# Song.all :limit => 2025 # large limits supported.
|
|
809
828
|
# Song.all :limit => :max # as many records as possible.
|
|
810
829
|
# @param count [Integer,Symbol,String,nil] The number of records to return.
|
|
811
|
-
# Pass
|
|
812
|
-
# Numeric strings (e.g.
|
|
813
|
-
# explicitly clear the limit. Any other value raises
|
|
830
|
+
# Pass `:max` to fetch as many records as possible (Parse-Server dependent).
|
|
831
|
+
# Numeric strings (e.g. `"50"`) are coerced to Integer. Pass `nil` to
|
|
832
|
+
# explicitly clear the limit. Any other value raises `ArgumentError`
|
|
814
833
|
# rather than silently disabling the limit.
|
|
815
834
|
# @return [self]
|
|
816
835
|
def limit(count)
|
|
@@ -855,18 +874,18 @@ module Parse
|
|
|
855
874
|
# Forces Parse Server (and the underlying MongoDB driver) to use the
|
|
856
875
|
# named index instead of the query planner's choice. Useful for
|
|
857
876
|
# benchmarking or for working around sub-optimal plan selection.
|
|
858
|
-
# The hint is emitted in the compiled REST query body as the
|
|
877
|
+
# The hint is emitted in the compiled REST query body as the `hint`
|
|
859
878
|
# parameter (supported by Parse Server 7.4.0+) AND forwarded to the
|
|
860
|
-
# mongo-direct path —
|
|
879
|
+
# mongo-direct path — `results_direct` / `count_direct` / `distinct_direct`
|
|
861
880
|
# pass it to {Parse::MongoDB.aggregate}/{Parse::MongoDB.find} as the Mongo
|
|
862
|
-
#
|
|
881
|
+
# `hint` option, so a plan diagnosed with {#explain} can be corrected on
|
|
863
882
|
# either path.
|
|
864
883
|
#
|
|
865
884
|
# @example Force a specific index
|
|
866
885
|
# Post.query(:status => "published").hint("status_1_created_at_-1").results
|
|
867
886
|
#
|
|
868
887
|
# @param index_name [String, nil, :_read_] the index name or key pattern to use,
|
|
869
|
-
# or
|
|
888
|
+
# or `nil` to clear a previously set hint. Called with no arguments acts as a
|
|
870
889
|
# reader and returns the current hint value.
|
|
871
890
|
# @return [String, nil, self]
|
|
872
891
|
HINT_UNSET = :_hint_unset_ # @!visibility private
|
|
@@ -1029,7 +1048,7 @@ module Parse
|
|
|
1029
1048
|
# # parts of a single where constraint
|
|
1030
1049
|
# { :column.constraint => value }
|
|
1031
1050
|
# @see Parse::Constraint
|
|
1032
|
-
# @param
|
|
1051
|
+
# @param expressions [Hash] a set of constraints for this query.
|
|
1033
1052
|
# @param opts [Hash] a set of options when adding the constraints. This is
|
|
1034
1053
|
# specific for each Parse::Constraint.
|
|
1035
1054
|
# @return [self]
|
|
@@ -1669,7 +1688,7 @@ module Parse
|
|
|
1669
1688
|
_id _created_at _updated_at _acl
|
|
1670
1689
|
].freeze
|
|
1671
1690
|
|
|
1672
|
-
# Recursively delete every key named in
|
|
1691
|
+
# Recursively delete every key named in `names` from a nested
|
|
1673
1692
|
# Hash/Array structure, in place. Symbol and string keys both match.
|
|
1674
1693
|
# @param value [Object] a Hash, Array, or scalar
|
|
1675
1694
|
# @param names [Set<String>] the key names to drop
|
|
@@ -2079,6 +2098,39 @@ module Parse
|
|
|
2079
2098
|
end
|
|
2080
2099
|
end
|
|
2081
2100
|
|
|
2101
|
+
# Auth kwargs for the Atlas Search bridge (`#atlas_search` builder
|
|
2102
|
+
# block). Explicit `atlas_search(...)` auth kwargs win; otherwise
|
|
2103
|
+
# derive from the query's own scope (`#scope_to_user`, an explicit
|
|
2104
|
+
# `session_token`, an ambient `Parse.with_session`, or `use_master_key`).
|
|
2105
|
+
#
|
|
2106
|
+
# Unlike {#mongo_direct_auth_kwargs} there is deliberately NO silent
|
|
2107
|
+
# `{ master: true }` fallback for an unscoped query — we return `{}`
|
|
2108
|
+
# so `Parse::AtlasSearch.search_with_stage` applies Atlas Search's own
|
|
2109
|
+
# policy (`resolve_scope!`): public-only fallback, or `ACLRequired`
|
|
2110
|
+
# when `Parse::AtlasSearch.require_session_token` is on. That keeps the
|
|
2111
|
+
# search bridge fail-closed rather than defaulting an unscoped block
|
|
2112
|
+
# search to a master-key ACL bypass.
|
|
2113
|
+
# @return [Hash] zero-or-one-of { session_token:/master:/acl_user:/acl_role: }
|
|
2114
|
+
# @!visibility private
|
|
2115
|
+
def atlas_search_auth_kwargs(options)
|
|
2116
|
+
explicit = %i[session_token master acl_user acl_role].select { |k| options.key?(k) }
|
|
2117
|
+
return explicit.to_h { |k| [k, options[k]] } if explicit.any?
|
|
2118
|
+
|
|
2119
|
+
if @acl_user
|
|
2120
|
+
{ acl_user: @acl_user }
|
|
2121
|
+
elsif @acl_role
|
|
2122
|
+
{ acl_role: @acl_role }
|
|
2123
|
+
elsif @session_token.is_a?(String) && !@session_token.empty?
|
|
2124
|
+
{ session_token: @session_token }
|
|
2125
|
+
elsif use_master_key == true
|
|
2126
|
+
{ master: true }
|
|
2127
|
+
elsif (ambient = ambient_session_token)
|
|
2128
|
+
{ session_token: ambient }
|
|
2129
|
+
else
|
|
2130
|
+
{}
|
|
2131
|
+
end
|
|
2132
|
+
end
|
|
2133
|
+
|
|
2082
2134
|
# The fiber-local ambient session token set by `Parse.with_session`,
|
|
2083
2135
|
# or nil. A whitespace-only ambient is treated as absent so it cannot
|
|
2084
2136
|
# block the master fallback and then fail a later presence check —
|
|
@@ -2140,7 +2192,7 @@ module Parse
|
|
|
2140
2192
|
# instead of Parse::Object instances (default: false)
|
|
2141
2193
|
# @param max_time_ms [Integer, nil] optional server-side time limit in milliseconds.
|
|
2142
2194
|
# When provided, MongoDB will cancel the aggregation if it exceeds this budget and
|
|
2143
|
-
# {Parse::MongoDB::ExecutionTimeout} is raised. Pass
|
|
2195
|
+
# {Parse::MongoDB::ExecutionTimeout} is raised. Pass `nil` (the default) for no cap.
|
|
2144
2196
|
# @yield a block to iterate for each object that matched the query
|
|
2145
2197
|
# @return [Array<Parse::Object>] if raw is false, a list of Parse::Object subclasses
|
|
2146
2198
|
# @return [Array<Hash>] if raw is true, Parse-formatted JSON hashes
|
|
@@ -2180,7 +2232,7 @@ module Parse
|
|
|
2180
2232
|
|
|
2181
2233
|
# Execute the aggregation directly on MongoDB. The pipeline was built
|
|
2182
2234
|
# entirely from SDK constraint translation (no user-supplied stages),
|
|
2183
|
-
# so legitimate
|
|
2235
|
+
# so legitimate `_rperm`/`_wperm` references emitted by
|
|
2184
2236
|
# {#readable_by_role} and friends are sanctioned. The DENIED_OPERATORS
|
|
2185
2237
|
# walk still runs at the MongoDB layer. When `session_token:` or
|
|
2186
2238
|
# `master:` is supplied, Parse::MongoDB.aggregate adds the
|
|
@@ -2491,55 +2543,73 @@ module Parse
|
|
|
2491
2543
|
skip_val = options[:skip] || (@skip > 0 ? @skip : 0)
|
|
2492
2544
|
|
|
2493
2545
|
if block_given?
|
|
2494
|
-
# Builder
|
|
2546
|
+
# Builder-block mode: the caller configures the $search stage
|
|
2547
|
+
# directly via the yielded SearchBuilder. Route through
|
|
2548
|
+
# Parse::AtlasSearch.search_with_stage so the SAME SDK-side
|
|
2549
|
+
# enforcement chain as the options API runs — scope resolution,
|
|
2550
|
+
# CLP `find`, the ACL $match placed AFTER $search (not prepended
|
|
2551
|
+
# to stage 0), protectedFields/pointerFields redaction, and the
|
|
2552
|
+
# $expr protected-field oracle guard on the caller filter.
|
|
2553
|
+
#
|
|
2554
|
+
# The previous implementation called Parse::MongoDB.aggregate
|
|
2555
|
+
# with `allow_internal_fields: true` and NO auth forwarding: it
|
|
2556
|
+
# both ran unscoped (resolved to :public / master, no ACL) and,
|
|
2557
|
+
# because that helper prepends the ACL $match at position 0, was
|
|
2558
|
+
# rejected outright by Atlas (which requires $search at stage 0).
|
|
2559
|
+
# It was non-functional under both supported security configs.
|
|
2495
2560
|
index_name = options[:index] || Parse::AtlasSearch.default_index
|
|
2496
2561
|
builder = Parse::AtlasSearch::SearchBuilder.new(index_name: index_name)
|
|
2497
2562
|
yield builder
|
|
2498
2563
|
|
|
2499
|
-
#
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
#
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
#
|
|
2564
|
+
# Carry the query's existing constraints as the caller filter,
|
|
2565
|
+
# interpolated into a post-$search $match. The constraints MUST be
|
|
2566
|
+
# converted to Mongo storage form first: `compile_where` yields the
|
|
2567
|
+
# Parse REST shape (pointer keys like `owner`, values that are live
|
|
2568
|
+
# Parse::Pointer objects / Parse date dicts). Fed to a `$match`
|
|
2569
|
+
# verbatim, a Parse::Pointer fails BSON serialization and dates/ids
|
|
2570
|
+
# target the wrong field (`owner` vs `_p_owner`). `convert_-
|
|
2571
|
+
# constraints_for_direct_mongodb` rewrites keys/values to storage
|
|
2572
|
+
# form — the same conversion the mongo-direct path applies, and the
|
|
2573
|
+
# one the pre-5.5.5 block path used before this branch was rewritten.
|
|
2574
|
+
filter = options[:filter]
|
|
2506
2575
|
compiled_where = compile_where
|
|
2507
2576
|
if compiled_where.present?
|
|
2508
2577
|
regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" }
|
|
2509
2578
|
if regular_constraints.any?
|
|
2510
2579
|
mongo_constraints = convert_constraints_for_direct_mongodb(regular_constraints)
|
|
2511
|
-
|
|
2580
|
+
filter = (filter || {}).merge(mongo_constraints)
|
|
2512
2581
|
end
|
|
2513
2582
|
end
|
|
2514
2583
|
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2584
|
+
search_opts = {
|
|
2585
|
+
filter: filter,
|
|
2586
|
+
limit: limit,
|
|
2587
|
+
skip: skip_val,
|
|
2588
|
+
class_name: @table,
|
|
2589
|
+
raw: options[:raw],
|
|
2590
|
+
**atlas_search_auth_kwargs(options),
|
|
2591
|
+
}
|
|
2592
|
+
rp = options.key?(:read_preference) ? options[:read_preference] : @read_preference
|
|
2593
|
+
search_opts[:read_preference] = rp unless rp.nil?
|
|
2594
|
+
search_opts[:max_time_ms] = options[:max_time_ms] if options.key?(:max_time_ms)
|
|
2525
2595
|
|
|
2526
|
-
|
|
2527
|
-
if options[:raw]
|
|
2528
|
-
Parse::AtlasSearch::SearchResult.new(results: raw_results, raw_results: raw_results)
|
|
2529
|
-
else
|
|
2530
|
-
parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table)
|
|
2531
|
-
objects = parse_results.map { |doc| Parse.decode(doc) }.compact
|
|
2532
|
-
Parse::AtlasSearch::SearchResult.new(results: objects, raw_results: raw_results)
|
|
2533
|
-
end
|
|
2596
|
+
Parse::AtlasSearch.search_with_stage(@table, builder.build, **search_opts)
|
|
2534
2597
|
else
|
|
2535
2598
|
# Simple options API - delegate to AtlasSearch module
|
|
2536
2599
|
raise ArgumentError, "query string is required when not using a block" if query.nil?
|
|
2537
2600
|
|
|
2538
|
-
# Merge query constraints as filter
|
|
2601
|
+
# Merge query constraints as filter. Convert to Mongo storage form
|
|
2602
|
+
# first (same reasoning as the block branch above): compile_where
|
|
2603
|
+
# yields the Parse REST shape (live Parse::Pointer objects, Parse
|
|
2604
|
+
# date dicts, un-prefixed pointer field names) which a raw $match
|
|
2605
|
+
# cannot serialize / would target the wrong column.
|
|
2539
2606
|
compiled_where = compile_where
|
|
2540
2607
|
if compiled_where.present?
|
|
2541
2608
|
regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" }
|
|
2542
|
-
|
|
2609
|
+
if regular_constraints.any?
|
|
2610
|
+
mongo_constraints = convert_constraints_for_direct_mongodb(regular_constraints)
|
|
2611
|
+
options[:filter] = (options[:filter] || {}).merge(mongo_constraints)
|
|
2612
|
+
end
|
|
2543
2613
|
end
|
|
2544
2614
|
|
|
2545
2615
|
options[:class_name] = @table
|
|
@@ -2596,11 +2666,17 @@ module Parse
|
|
|
2596
2666
|
"Call Parse::AtlasSearch.configure(enabled: true) after configuring Parse::MongoDB."
|
|
2597
2667
|
end
|
|
2598
2668
|
|
|
2599
|
-
# Merge query constraints as filter
|
|
2669
|
+
# Merge query constraints as filter, converted to Mongo storage form
|
|
2670
|
+
# first (see #atlas_search for the reasoning — a raw Parse::Pointer
|
|
2671
|
+
# fails BSON serialization and un-prefixed pointer fields target the
|
|
2672
|
+
# wrong column).
|
|
2600
2673
|
compiled_where = compile_where
|
|
2601
2674
|
if compiled_where.present?
|
|
2602
2675
|
regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" }
|
|
2603
|
-
|
|
2676
|
+
if regular_constraints.any?
|
|
2677
|
+
mongo_constraints = convert_constraints_for_direct_mongodb(regular_constraints)
|
|
2678
|
+
options[:filter] = (options[:filter] || {}).merge(mongo_constraints)
|
|
2679
|
+
end
|
|
2604
2680
|
end
|
|
2605
2681
|
|
|
2606
2682
|
# Use query limit if set and no explicit limit provided
|
|
@@ -5964,22 +6040,22 @@ module Parse
|
|
|
5964
6040
|
# @param verbose [Boolean, nil] whether to print verbose output (nil means use query's setting)
|
|
5965
6041
|
# @param mongo_direct [Boolean] if true, uses MongoDB directly bypassing Parse Server (required for $literal)
|
|
5966
6042
|
# @param max_time_ms [Integer, nil] optional server-side time limit in milliseconds passed to
|
|
5967
|
-
# {Parse::MongoDB.aggregate} when mongo_direct is true. Pass
|
|
5968
|
-
# @param raw_values [Boolean] when true, passes
|
|
6043
|
+
# {Parse::MongoDB.aggregate} when mongo_direct is true. Pass `nil` (the default) for no cap.
|
|
6044
|
+
# @param raw_values [Boolean] when true, passes `rawValues: true` to the Parse Server REST
|
|
5969
6045
|
# aggregate endpoint (PS 9.9.0+). Has no effect on the mongo-direct path.
|
|
5970
|
-
# @param raw_field_names [Boolean] when true, passes
|
|
6046
|
+
# @param raw_field_names [Boolean] when true, passes `rawFieldNames: true` to the Parse Server
|
|
5971
6047
|
# REST aggregate endpoint (PS 9.9.0+). Has no effect on the mongo-direct path.
|
|
5972
6048
|
# @param allow_internal_fields [Boolean] when true, the mongo-direct path
|
|
5973
|
-
# forwards
|
|
5974
|
-
# SDK-built ACL `$match` stages that legitimately reference
|
|
5975
|
-
#
|
|
6049
|
+
# forwards `allow_internal_fields: true` to {Parse::MongoDB.aggregate} so
|
|
6050
|
+
# SDK-built ACL `$match` stages that legitimately reference `_rperm` /
|
|
6051
|
+
# `_wperm` (emitted by {Parse::Query#readable_by}, `#publicly_readable`,
|
|
5976
6052
|
# and friends) pass the pipeline-security internal-fields denylist —
|
|
5977
|
-
# matching the parity already held by
|
|
5978
|
-
#
|
|
6053
|
+
# matching the parity already held by `results_direct` / `count_direct` /
|
|
6054
|
+
# `distinct_direct`. Set `true` ONLY when this Aggregation's pipeline was
|
|
5979
6055
|
# built entirely from SDK constraint translation (no caller-supplied
|
|
5980
6056
|
# stages); the credential-field guard (`_hashed_password`, session tokens,
|
|
5981
|
-
# auth data) is what
|
|
5982
|
-
# set on a pipeline that interpolates user input. Defaults to
|
|
6057
|
+
# auth data) is what `allow_internal_fields` relaxes, so it must never be
|
|
6058
|
+
# set on a pipeline that interpolates user input. Defaults to `false`.
|
|
5983
6059
|
def initialize(query, pipeline, verbose: nil, mongo_direct: false, max_time_ms: nil,
|
|
5984
6060
|
raw_values: false, raw_field_names: false, allow_internal_fields: false)
|
|
5985
6061
|
@query = query
|
|
@@ -6041,7 +6117,7 @@ module Parse
|
|
|
6041
6117
|
|
|
6042
6118
|
# Execute aggregation directly on MongoDB
|
|
6043
6119
|
# @param max_time_ms [Integer, nil] optional server-side time limit (milliseconds).
|
|
6044
|
-
# Defaults to the value passed to {#initialize} via the
|
|
6120
|
+
# Defaults to the value passed to {#initialize} via the `max_time_ms:` keyword.
|
|
6045
6121
|
# @return [Array<Hash>] raw MongoDB results
|
|
6046
6122
|
def execute_direct!(max_time_ms: @max_time_ms)
|
|
6047
6123
|
table = @query.instance_variable_get(:@table)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "uri"
|
|
6
|
+
require "ipaddr"
|
|
6
7
|
require_relative "../reranker"
|
|
7
8
|
|
|
8
9
|
module Parse
|
|
@@ -197,10 +198,39 @@ module Parse
|
|
|
197
198
|
unless uri.is_a?(URI::HTTPS) || uri.is_a?(URI::HTTP)
|
|
198
199
|
raise ArgumentError, "Reranker::Cohere: base_url must be http(s) (got #{base_url.inspect})."
|
|
199
200
|
end
|
|
201
|
+
# Never accept credentials embedded in the URL. They leak into
|
|
202
|
+
# logs / error messages / the Faraday connection, and userinfo
|
|
203
|
+
# can mask the real host (`https://evil@api.cohere.com/`).
|
|
204
|
+
unless uri.userinfo.nil?
|
|
205
|
+
raise ArgumentError,
|
|
206
|
+
"Reranker::Cohere: base_url must not embed userinfo (credentials in the URL)."
|
|
207
|
+
end
|
|
208
|
+
# Plaintext http:// would send the Cohere API key in the clear.
|
|
209
|
+
# Permit it ONLY for loopback hosts (a local dev proxy / sidecar);
|
|
210
|
+
# require https:// for anything that leaves the machine. NB:
|
|
211
|
+
# URI::HTTPS is a subclass of URI::HTTP, so match on the scheme
|
|
212
|
+
# string, not `is_a?`.
|
|
213
|
+
if uri.scheme == "http" && !loopback_host?(uri.host)
|
|
214
|
+
raise ArgumentError,
|
|
215
|
+
"Reranker::Cohere: base_url must be https:// for non-loopback hosts " \
|
|
216
|
+
"(refusing to send the API key over plaintext http to #{uri.host.inspect})."
|
|
217
|
+
end
|
|
200
218
|
rescue URI::InvalidURIError => e
|
|
201
219
|
raise ArgumentError, "Reranker::Cohere: invalid base_url #{base_url.inspect} (#{e.message})."
|
|
202
220
|
end
|
|
203
221
|
|
|
222
|
+
# @return [Boolean] true for localhost / 127.0.0.0/8 / ::1 etc.
|
|
223
|
+
def loopback_host?(host)
|
|
224
|
+
return false if host.nil? || host.empty?
|
|
225
|
+
h = host.downcase.sub(/\A\[/, "").sub(/\]\z/, "") # strip IPv6 brackets
|
|
226
|
+
return true if h == "localhost"
|
|
227
|
+
begin
|
|
228
|
+
IPAddr.new(h).loopback?
|
|
229
|
+
rescue IPAddr::Error
|
|
230
|
+
false
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
204
234
|
def safe_base_host
|
|
205
235
|
URI.parse(@base_url).host
|
|
206
236
|
rescue StandardError
|
data/lib/parse/schema.rb
CHANGED
|
@@ -523,7 +523,7 @@ module Parse
|
|
|
523
523
|
# via the conventional accessor names some Parse-Stack models
|
|
524
524
|
# ship with), OR
|
|
525
525
|
# - {Parse::Schema.default_class_level_permissions} if set, OR
|
|
526
|
-
# -
|
|
526
|
+
# - `nil` to leave `classLevelPermissions` off the schema body so
|
|
527
527
|
# Parse Server uses its built-in defaults.
|
|
528
528
|
def model_class_level_permissions
|
|
529
529
|
%i[class_level_permissions classLevelPermissions clp].each do |reader|
|
data/lib/parse/stack/version.rb
CHANGED
data/lib/parse/stack.rb
CHANGED
|
@@ -44,14 +44,14 @@ module Parse
|
|
|
44
44
|
|
|
45
45
|
# Fiber-local key consulted by the authentication middleware. A truthy
|
|
46
46
|
# entry suppresses the master-key header for the duration of the block
|
|
47
|
-
# set by {Parse.without_master_key}; a
|
|
47
|
+
# set by {Parse.without_master_key}; a `:enabled` entry forces the
|
|
48
48
|
# master-key header back on inside a nested {Parse.with_master_key}
|
|
49
49
|
# block.
|
|
50
50
|
MASTER_KEY_STATE_KEY = :__parse_master_key_state__
|
|
51
51
|
|
|
52
|
-
# Run
|
|
52
|
+
# Run `block` with the master key suppressed for every Parse request
|
|
53
53
|
# originating in the current fiber. Equivalent to setting the
|
|
54
|
-
#
|
|
54
|
+
# `X-Disable-Parse-Master-Key` header on each request, but block-scoped
|
|
55
55
|
# so callers can wrap a unit of work — e.g. running an action "as if
|
|
56
56
|
# the configured master key were not available" — without threading
|
|
57
57
|
# the header through every intermediate call.
|
|
@@ -105,7 +105,7 @@ module Parse
|
|
|
105
105
|
# the previous value on exit.
|
|
106
106
|
SESSION_TOKEN_STATE_KEY = :__parse_session_token__
|
|
107
107
|
|
|
108
|
-
# Run
|
|
108
|
+
# Run `block` with an ambient session token set for the current fiber.
|
|
109
109
|
# Inside the block, every Parse request that doesn't explicitly pass
|
|
110
110
|
# `session_token:` *and* doesn't explicitly request `use_master_key:
|
|
111
111
|
# true` will be sent with this token. Equivalent to threading
|
|
@@ -140,8 +140,21 @@ module Parse
|
|
|
140
140
|
def self.with_session(token)
|
|
141
141
|
resolved = token.respond_to?(:session_token) ? token.session_token : token
|
|
142
142
|
resolved = resolved.to_s if resolved
|
|
143
|
+
# Capture BEFORE any raise so the `ensure` always restores the real
|
|
144
|
+
# previous ambient (never clobbers an enclosing with_session).
|
|
143
145
|
previous = Fiber[SESSION_TOKEN_STATE_KEY]
|
|
144
|
-
|
|
146
|
+
# SEC-02: a present-but-blank (empty or whitespace) token is an unusable
|
|
147
|
+
# credential. The prior behavior stored a whitespace token as the ambient
|
|
148
|
+
# (only an exactly-empty string was treated as absent), and the request
|
|
149
|
+
# layer would then drop it and silently send the master key. Reject blank
|
|
150
|
+
# tokens loudly at the source instead. `nil` still means "no ambient".
|
|
151
|
+
if resolved.is_a?(String) && resolved.strip.empty?
|
|
152
|
+
raise ArgumentError,
|
|
153
|
+
"Parse.with_session was given a blank session token. A present-but-empty " \
|
|
154
|
+
"token is refused so the block cannot silently execute with master-key " \
|
|
155
|
+
"authority — pass a valid session token, or `nil` for no ambient session."
|
|
156
|
+
end
|
|
157
|
+
Fiber[SESSION_TOKEN_STATE_KEY] = resolved
|
|
145
158
|
yield
|
|
146
159
|
ensure
|
|
147
160
|
Fiber[SESSION_TOKEN_STATE_KEY] = previous
|
|
@@ -584,11 +597,11 @@ module Parse
|
|
|
584
597
|
# nil, falls back to {Parse.cache}.
|
|
585
598
|
#
|
|
586
599
|
# SECURITY: if you pass a raw Moneta-Redis store, build it with
|
|
587
|
-
#
|
|
588
|
-
# token back (
|
|
589
|
-
# Marshal value serializer that read
|
|
600
|
+
# `value_serializer: nil`. The lock release path reads the stored owner
|
|
601
|
+
# token back (`store[key]`) to compare-and-delete; with Moneta's default
|
|
602
|
+
# Marshal value serializer that read `Marshal.load`s bytes from Redis — an
|
|
590
603
|
# RCE vector on a shared/untrusted/MITM'd lock store. With
|
|
591
|
-
#
|
|
604
|
+
# `value_serializer: nil` the owner token is a plain string and is never
|
|
592
605
|
# deserialized. Alternatively pass a {Parse::Cache::Redis} instance, which
|
|
593
606
|
# uses a raw-string acquire/release path and avoids Marshal entirely.
|
|
594
607
|
# @example
|
|
@@ -600,7 +613,7 @@ module Parse
|
|
|
600
613
|
# Optional allowlist of {Parse::Object} subclasses that may use the
|
|
601
614
|
# synchronize-create lock. When set, calls from any other class raise
|
|
602
615
|
# {Parse::CreateLockUnavailableError}. When nil (default) with the global
|
|
603
|
-
# default enabled, a one-time
|
|
616
|
+
# default enabled, a one-time `[Parse::Stack:SECURITY]` warning is emitted
|
|
604
617
|
# noting the unbounded surface; the lock still applies to every class.
|
|
605
618
|
#
|
|
606
619
|
# **Inheritance behavior:** The allowlist check in
|
|
@@ -318,12 +318,12 @@ module Parse
|
|
|
318
318
|
# CONFIRM the disable took effect from the SERVER's own view — a
|
|
319
319
|
# positive post-condition rather than trusting the unlink response
|
|
320
320
|
# alone. We must read the server directly here, NOT lean on the
|
|
321
|
-
# in-memory #mfa_enabled? projection: Parse Server omits
|
|
321
|
+
# in-memory #mfa_enabled? projection: Parse Server omits `authData`
|
|
322
322
|
# entirely for a user with no providers, so once MFA is unlinked an
|
|
323
|
-
# ordinary fetch carries no
|
|
324
|
-
# never clear the
|
|
323
|
+
# ordinary fetch carries no `authData` key at all and therefore can
|
|
324
|
+
# never clear the `{ mfa: { status: "enabled" } }` value pinned at
|
|
325
325
|
# enrollment. An enabled account's own (session-token) read returns
|
|
326
|
-
#
|
|
326
|
+
# `authData.mfa`; a disabled one omits it — so an absent/mfa-less
|
|
327
327
|
# authData on this trusted self-read is the authoritative signal.
|
|
328
328
|
if mfa_enabled_on_server?
|
|
329
329
|
raise MFA::VerificationError, "MFA disable did not take effect (still enabled after unlink)"
|
|
@@ -337,27 +337,27 @@ module Parse
|
|
|
337
337
|
# verification entirely, so the caller must prove (out-of-band) that
|
|
338
338
|
# the operator initiating the disable is authorized to do so.
|
|
339
339
|
#
|
|
340
|
-
# The
|
|
340
|
+
# The `authorized_by:` keyword is required and must be a
|
|
341
341
|
# {Parse::User} (or {Parse::Pointer} to a User) representing the
|
|
342
342
|
# operator performing the override. The caller is responsible for
|
|
343
343
|
# verifying that operator's privileges (e.g. via a role check). An
|
|
344
|
-
# optional
|
|
344
|
+
# optional `admin_role:` argument lets this method enforce a role
|
|
345
345
|
# membership check on the operator using the existing role-hierarchy
|
|
346
346
|
# support; when given, the operator must belong to the role (or any
|
|
347
|
-
# of its child roles) or
|
|
347
|
+
# of its child roles) or `ForbiddenError` is raised.
|
|
348
348
|
#
|
|
349
349
|
# @param authorized_by [Parse::User, Parse::Pointer] the operator
|
|
350
350
|
# performing the override. Required.
|
|
351
351
|
# @param admin_role [Parse::Role, String, nil] role (or role name)
|
|
352
|
-
# that
|
|
353
|
-
# this or
|
|
352
|
+
# that `authorized_by` must belong to. Library-enforced. Either
|
|
353
|
+
# this or `allow_unverified: true` is REQUIRED (fail-closed).
|
|
354
354
|
# @param allow_unverified [Boolean] explicitly accept caller-side
|
|
355
|
-
# authorization without a library role check. Defaults to
|
|
356
|
-
# must be set deliberately to bypass MFA without an
|
|
355
|
+
# authorization without a library role check. Defaults to `false`;
|
|
356
|
+
# must be set deliberately to bypass MFA without an `admin_role`.
|
|
357
357
|
# @return [Boolean] True if disabled successfully.
|
|
358
|
-
# @raise [ArgumentError] when
|
|
359
|
-
# @raise [Parse::MFA::ForbiddenError] when neither
|
|
360
|
-
#
|
|
358
|
+
# @raise [ArgumentError] when `authorized_by:` is missing or not a User.
|
|
359
|
+
# @raise [Parse::MFA::ForbiddenError] when neither `admin_role` nor
|
|
360
|
+
# `allow_unverified:` is supplied, or when `admin_role` is supplied
|
|
361
361
|
# and the operator is not a member.
|
|
362
362
|
#
|
|
363
363
|
# @example Library-enforced role check (preferred)
|
|
@@ -415,8 +415,8 @@ module Parse
|
|
|
415
415
|
end
|
|
416
416
|
|
|
417
417
|
# Refresh auth_data, then drop the in-memory MFA projection. As in
|
|
418
|
-
# #disable_mfa!, a disabled user's read omits
|
|
419
|
-
#
|
|
418
|
+
# #disable_mfa!, a disabled user's read omits `authData`, so the
|
|
419
|
+
# `{ mfa: { status: "enabled" } }` value pinned at enrollment won't
|
|
420
420
|
# self-clear on fetch — clear it explicitly so #mfa_enabled? reports
|
|
421
421
|
# the truth after a master-key disable.
|
|
422
422
|
fetch
|
|
@@ -426,7 +426,7 @@ module Parse
|
|
|
426
426
|
end
|
|
427
427
|
|
|
428
428
|
# @deprecated Use {#disable_mfa_master_key!} with an explicit
|
|
429
|
-
#
|
|
429
|
+
# `authorized_by:` argument. The old name had no authorization gate
|
|
430
430
|
# and acted as a one-call IDOR primitive when invoked on an
|
|
431
431
|
# attacker-controlled user instance.
|
|
432
432
|
def disable_mfa_admin!(*args, **kwargs)
|
|
@@ -496,10 +496,10 @@ module Parse
|
|
|
496
496
|
|
|
497
497
|
# @!visibility private
|
|
498
498
|
# Authoritative server-side MFA check via a trusted self-read.
|
|
499
|
-
# Reads
|
|
499
|
+
# Reads `authData.mfa` straight from a fresh session-token fetch
|
|
500
500
|
# rather than the (possibly stale) in-memory projection. An enabled
|
|
501
|
-
# account returns
|
|
502
|
-
# one omits
|
|
501
|
+
# account returns `authData.mfa` with a `status`/`secret`; a disabled
|
|
502
|
+
# one omits `authData` — so absence (or an mfa-less authData) means
|
|
503
503
|
# disabled.
|
|
504
504
|
# @return [Boolean]
|
|
505
505
|
def mfa_enabled_on_server?
|
|
@@ -512,13 +512,13 @@ module Parse
|
|
|
512
512
|
|
|
513
513
|
# @!visibility private
|
|
514
514
|
# Drop the in-memory MFA projection after a disable. A disabled user's
|
|
515
|
-
# server read omits
|
|
516
|
-
# never clear the
|
|
517
|
-
# enrollment; do it explicitly here. Only the
|
|
515
|
+
# server read omits `authData` entirely, so an ordinary fetch can
|
|
516
|
+
# never clear the `{ mfa: { status: "enabled" } }` value pinned at
|
|
517
|
+
# enrollment; do it explicitly here. Only the `mfa` subkey is removed
|
|
518
518
|
# (any anonymous/OAuth authData is preserved), and the assignment runs
|
|
519
|
-
# through the non-dirtying hydration path inside a
|
|
519
|
+
# through the non-dirtying hydration path inside a `with_authdata_trust`
|
|
520
520
|
# scope so it is neither stripped nor marked dirty — a later #save will
|
|
521
|
-
# not resend
|
|
521
|
+
# not resend `authData`.
|
|
522
522
|
def clear_local_mfa_projection!
|
|
523
523
|
cleared = auth_data.is_a?(Hash) ? auth_data.dup : {}
|
|
524
524
|
cleared.delete("mfa")
|