parse-stack-next 5.5.3 → 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 +13 -4
- 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/model/object.rb
CHANGED
|
@@ -85,32 +85,32 @@ module Parse
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# This is the core class for all app specific Parse table subclasses. This class
|
|
88
|
-
#
|
|
88
|
+
# inherits from Parse::Pointer since an Object is a Parse::Pointer with additional fields,
|
|
89
89
|
# at a minimum, created_at, updated_at and ACLs. This class also handles all
|
|
90
90
|
# the relational types of associations in a Parse application and handles the main CRUD operations.
|
|
91
91
|
#
|
|
92
92
|
# As the parent class to all custom subclasses, this class provides the default property schema:
|
|
93
93
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
94
|
+
# class Parse::Object
|
|
95
|
+
# # All subclasses will inherit these properties by default.
|
|
96
96
|
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
97
|
+
# # the objectId column of a record.
|
|
98
|
+
# property :id, :string, field: :objectId
|
|
99
99
|
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
100
|
+
# # The last updated date for a record (Parse::Date)
|
|
101
|
+
# property :updated_at, :date
|
|
102
102
|
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
103
|
+
# # The original creation date of a record (Parse::Date)
|
|
104
|
+
# property :created_at, :date
|
|
105
105
|
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
106
|
+
# # The Parse::ACL field
|
|
107
|
+
# property :acl, :acl, field: :ACL
|
|
108
108
|
#
|
|
109
|
-
#
|
|
109
|
+
# end
|
|
110
110
|
#
|
|
111
111
|
# Most Pointers and Object subclasses are treated the same. Therefore, defining a class Artist < Parse::Object
|
|
112
112
|
# that only has `id` set, will be treated as a pointer. Therefore a Parse::Object can be in a "pointer" state
|
|
113
|
-
# based on the data that it contains.
|
|
113
|
+
# based on the data that it contains. Because of this, it is possible to take an Artist instance
|
|
114
114
|
# (in this example), that is in a pointer state, and fetch the rest of the data for that particular
|
|
115
115
|
# record without having to create a new object. Doing so would now mark it as not being a pointer anymore.
|
|
116
116
|
# This is important to the understanding on how relations and properties are handled.
|
|
@@ -125,7 +125,7 @@ module Parse
|
|
|
125
125
|
#
|
|
126
126
|
# Associations:
|
|
127
127
|
#
|
|
128
|
-
# Parse supports
|
|
128
|
+
# Parse supports three main types of relational associations. One type of
|
|
129
129
|
# relation is the `One-to-One` association. This is implemented through a
|
|
130
130
|
# specific column in Parse with a Pointer data type. This pointer column,
|
|
131
131
|
# contains a local value that refers to a different record in a separate Parse
|
|
@@ -1329,25 +1329,25 @@ module Parse
|
|
|
1329
1329
|
#
|
|
1330
1330
|
# @param hash [Hash] the hash representing the object.
|
|
1331
1331
|
# Untrusted by default: keys in
|
|
1332
|
-
# {Parse::Properties::PROTECTED_INITIALIZE_KEYS} (
|
|
1333
|
-
#
|
|
1334
|
-
# are filtered out even when an
|
|
1335
|
-
# closes the mass-assignment hole where
|
|
1336
|
-
# on a hash that happens to include
|
|
1332
|
+
# {Parse::Properties::PROTECTED_INITIALIZE_KEYS} (`sessionToken`,
|
|
1333
|
+
# `_rperm`, `_wperm`, `_hashed_password`, `authData`, `roles`)
|
|
1334
|
+
# are filtered out even when an `objectId` is present. This
|
|
1335
|
+
# closes the mass-assignment hole where `klass.new(attacker_params)`
|
|
1336
|
+
# on a hash that happens to include `objectId` would overwrite
|
|
1337
1337
|
# session tokens, ACLs, and auth data. Use {Parse::Object.build}
|
|
1338
1338
|
# for trusted hydration from server JSON; it bypasses the filter.
|
|
1339
1339
|
# @return [Parse::Object] a the corresponding Parse::Object or subclass.
|
|
1340
1340
|
def initialize(opts = {})
|
|
1341
|
-
# Trusted hydration is signalled by the
|
|
1342
|
-
# variable rather than by a
|
|
1343
|
-
# keyword would break subclasses that override
|
|
1344
|
-
# and call
|
|
1345
|
-
# kwarg into a positional Hash through the variadic
|
|
1346
|
-
# and the subsequent
|
|
1341
|
+
# Trusted hydration is signalled by the `@_trusted_init` instance
|
|
1342
|
+
# variable rather than by a `trusted:` keyword argument. Using a
|
|
1343
|
+
# keyword would break subclasses that override `initialize(*args)`
|
|
1344
|
+
# and call `super` — Ruby 3 keyword-arg semantics would convert the
|
|
1345
|
+
# kwarg into a positional Hash through the variadic `*args` splat
|
|
1346
|
+
# and the subsequent `super` would arrive at this method with two
|
|
1347
1347
|
# positional args. The internal hydration paths
|
|
1348
1348
|
# ({Parse::Object.build}, {Parse::Pointer} autofetch,
|
|
1349
|
-
# {Parse::User#session})
|
|
1350
|
-
# invoke
|
|
1349
|
+
# {Parse::User#session}) `allocate` the object, set the ivar, then
|
|
1350
|
+
# invoke `initialize` so subclass overrides still fire and pick up
|
|
1351
1351
|
# the trust signal here.
|
|
1352
1352
|
trusted = @_trusted_init == true
|
|
1353
1353
|
@_trusted_init = nil
|
|
@@ -1369,7 +1369,7 @@ module Parse
|
|
|
1369
1369
|
# sessionToken / _rperm / _wperm / _hashed_password / authData /
|
|
1370
1370
|
# roles. The narrow list deliberately allows createdAt /
|
|
1371
1371
|
# updatedAt / className / __type through so the legitimate
|
|
1372
|
-
#
|
|
1372
|
+
# `Klass.new("objectId" => id, "createdAt" => ts, …)`
|
|
1373
1373
|
# cache-rehydrate pattern keeps working.
|
|
1374
1374
|
apply_attributes!(opts,
|
|
1375
1375
|
dirty_track: !dirty_track,
|
|
@@ -1772,13 +1772,13 @@ module Parse
|
|
|
1772
1772
|
# @return [Parse::Object] an instance of the Parse subclass
|
|
1773
1773
|
def self.build(json, table = nil, fetched_keys: nil, nested_fetched_keys: nil)
|
|
1774
1774
|
# Precedence (most → least authoritative):
|
|
1775
|
-
# 1. Caller-supplied
|
|
1775
|
+
# 1. Caller-supplied `table` — caller knows the expected class
|
|
1776
1776
|
# (e.g. webhook payload routed to a typed handler, has_many that
|
|
1777
1777
|
# knows its declared target class).
|
|
1778
|
-
# 2. The subclass
|
|
1778
|
+
# 2. The subclass `parse_class` when invoked on a Parse::Object
|
|
1779
1779
|
# subclass directly (Song.build(json)).
|
|
1780
1780
|
# 3. The className inside the JSON — only trusted when neither of
|
|
1781
|
-
# the above is available (e.g. base-class
|
|
1781
|
+
# the above is available (e.g. base-class `Parse::Object.build`
|
|
1782
1782
|
# on untyped JSON).
|
|
1783
1783
|
# Warn on mismatch between an explicit caller class and the
|
|
1784
1784
|
# payload-supplied className so type-confusion attacks surface in
|
|
@@ -1823,9 +1823,9 @@ module Parse
|
|
|
1823
1823
|
# Trusted hydration: this path runs on server-side JSON (response
|
|
1824
1824
|
# bodies, webhook payloads that have already been scrubbed,
|
|
1825
1825
|
# autofetch results). Server responses legitimately include
|
|
1826
|
-
# protected keys like
|
|
1827
|
-
# the in-memory object. Untrusted
|
|
1828
|
-
# default to filter those keys. The
|
|
1826
|
+
# protected keys like `sessionToken`, `_rperm` that must populate
|
|
1827
|
+
# the in-memory object. Untrusted `klass.new(hash)` callers
|
|
1828
|
+
# default to filter those keys. The `@_trusted_init` ivar is the
|
|
1829
1829
|
# signal — see {#initialize} for why we don't use a kwarg.
|
|
1830
1830
|
o.instance_variable_set(:@_trusted_init, true)
|
|
1831
1831
|
o.send(:initialize, json)
|
|
@@ -2138,13 +2138,13 @@ class Array
|
|
|
2138
2138
|
# Parse::Pointer or are a JSON Parse hash. If it is a hash, a Pare::Object will be built from it
|
|
2139
2139
|
# if it constrains the proper fields. Non-convertible objects will be removed.
|
|
2140
2140
|
#
|
|
2141
|
-
# When
|
|
2142
|
-
# — incoming hash
|
|
2141
|
+
# When `className` is provided by the caller, it is treated as authoritative
|
|
2142
|
+
# — incoming hash `className` values are ignored. This blocks attacker-
|
|
2143
2143
|
# controlled type confusion when this helper is invoked from typed
|
|
2144
|
-
# associations (
|
|
2144
|
+
# associations (`has_many`, `belongs_to`) that already know the expected
|
|
2145
2145
|
# class.
|
|
2146
2146
|
#
|
|
2147
|
-
# When
|
|
2147
|
+
# When `className` is `nil` (caller is doing untyped array conversion),
|
|
2148
2148
|
# the helper falls back to the hash-supplied className for compatibility
|
|
2149
2149
|
# with raw JSON deserialization callers.
|
|
2150
2150
|
#
|
data/lib/parse/model/pointer.rb
CHANGED
|
@@ -86,7 +86,7 @@ module Parse
|
|
|
86
86
|
|
|
87
87
|
# Assign the Parse objectId. Empty / nil values are permitted (Pointer
|
|
88
88
|
# in unbound state); non-empty values must match {OBJECT_ID_FORMAT}.
|
|
89
|
-
# @raise [ArgumentError] when
|
|
89
|
+
# @raise [ArgumentError] when `value` is a non-empty string that does
|
|
90
90
|
# not match the format.
|
|
91
91
|
def id=(value)
|
|
92
92
|
if value.nil?
|
|
@@ -255,9 +255,9 @@ module Parse
|
|
|
255
255
|
obj = klass.build(result, parse_class, fetched_keys: top_level_keys, nested_fetched_keys: nested_keys.presence)
|
|
256
256
|
else
|
|
257
257
|
# Full fetch - create without partial fetch tracking. Trusted
|
|
258
|
-
# hydration:
|
|
259
|
-
# legitimately carries
|
|
260
|
-
# and other PROTECTED_MASS_ASSIGNMENT_KEYS. The
|
|
258
|
+
# hydration: `result` is the server response body, which
|
|
259
|
+
# legitimately carries `createdAt`/`updatedAt`/`sessionToken`
|
|
260
|
+
# and other PROTECTED_MASS_ASSIGNMENT_KEYS. The `@_trusted_init`
|
|
261
261
|
# ivar tells {Parse::Object#initialize} to skip the protected-key
|
|
262
262
|
# filter — see that method for why we don't use a kwarg.
|
|
263
263
|
obj = klass.allocate
|
data/lib/parse/model/push.rb
CHANGED
|
@@ -74,14 +74,14 @@ module Parse
|
|
|
74
74
|
# caller did not explicitly opt in via {Parse::Push.allow_broadcast}
|
|
75
75
|
# or per-instance {#broadcast!}.
|
|
76
76
|
#
|
|
77
|
-
# This is a fail-closed guard against the
|
|
78
|
-
#
|
|
77
|
+
# This is a fail-closed guard against the `to_audience` /
|
|
78
|
+
# `to_audience_id` class of footguns where a typo, deleted audience,
|
|
79
79
|
# or unset-param silently degrades a targeted push into a global one.
|
|
80
80
|
class BroadcastNotAllowed < StandardError; end
|
|
81
81
|
|
|
82
82
|
# Raised when {#to_audience} or {#to_audience_id} cannot resolve the
|
|
83
83
|
# requested audience. Previously these methods warned and returned
|
|
84
|
-
#
|
|
84
|
+
# `self`, which let the subsequent `send!` silently broadcast to every
|
|
85
85
|
# Installation. They now raise so typos and renames surface at the
|
|
86
86
|
# call site instead.
|
|
87
87
|
class AudienceNotFound < ArgumentError; end
|
|
@@ -89,9 +89,9 @@ module Parse
|
|
|
89
89
|
# @!attribute [rw] allow_broadcast
|
|
90
90
|
# Whether {Parse::Push} permits an unconstrained push (no `where`,
|
|
91
91
|
# no `channels`) to broadcast to every Installation. Defaults to
|
|
92
|
-
#
|
|
92
|
+
# `false` — sending an unconstrained push raises {BroadcastNotAllowed}.
|
|
93
93
|
#
|
|
94
|
-
# Set to
|
|
94
|
+
# Set to `true` at boot for apps that legitimately broadcast (e.g.,
|
|
95
95
|
# `Parse::Push.allow_broadcast = true`). Or opt in per-instance with
|
|
96
96
|
# {#broadcast!}, which is auditable in code review.
|
|
97
97
|
# @return [Boolean]
|
data/lib/parse/mongodb.rb
CHANGED
|
@@ -28,15 +28,15 @@ module Parse
|
|
|
28
28
|
# When writing aggregation pipelines for direct MongoDB queries, use MongoDB's native
|
|
29
29
|
# field naming conventions:
|
|
30
30
|
#
|
|
31
|
-
# - *Regular fields*: Use camelCase (e.g.,
|
|
32
|
-
# - *Pointer fields*: Use
|
|
33
|
-
# - *Built-in dates*: Use
|
|
34
|
-
# - *Field references*: Use
|
|
31
|
+
# - *Regular fields*: Use camelCase (e.g., `releaseDate`, `playCount`, `firstName`)
|
|
32
|
+
# - *Pointer fields*: Use `_p_` prefix (e.g., `_p_author`, `_p_album`)
|
|
33
|
+
# - *Built-in dates*: Use `_created_at` and `_updated_at`
|
|
34
|
+
# - *Field references*: Use `$fieldName` syntax (e.g., `$releaseDate`, `$_p_author`)
|
|
35
35
|
#
|
|
36
36
|
# Results are automatically converted to Ruby-friendly format:
|
|
37
|
-
# - Field names converted to snake_case (
|
|
38
|
-
# - Custom aggregation results wrapped in
|
|
39
|
-
# - Parse documents returned as proper
|
|
37
|
+
# - Field names converted to snake_case (`totalPlays` → `total_plays`)
|
|
38
|
+
# - Custom aggregation results wrapped in `AggregationResult` for method access
|
|
39
|
+
# - Parse documents returned as proper `Parse::Object` instances
|
|
40
40
|
#
|
|
41
41
|
# @example Aggregation pipeline with MongoDB field names
|
|
42
42
|
# pipeline = [
|
|
@@ -52,9 +52,9 @@ module Parse
|
|
|
52
52
|
# == Date Comparisons
|
|
53
53
|
#
|
|
54
54
|
# MongoDB stores dates in UTC. When comparing dates in aggregation pipelines:
|
|
55
|
-
# - Use Ruby
|
|
56
|
-
# - Ruby
|
|
57
|
-
# - For accurate date-only comparisons, use
|
|
55
|
+
# - Use Ruby `Time` objects for comparisons (automatically converted to BSON dates)
|
|
56
|
+
# - Ruby `Date` objects (without time) are stored as midnight UTC
|
|
57
|
+
# - For accurate date-only comparisons, use `Time.utc(year, month, day)`
|
|
58
58
|
#
|
|
59
59
|
# @example Date comparison in aggregation
|
|
60
60
|
# # Compare with a specific UTC time
|
|
@@ -1460,7 +1460,7 @@ module Parse
|
|
|
1460
1460
|
# @param max_time_ms [Integer, nil] optional server-side time limit in milliseconds.
|
|
1461
1461
|
# When provided, MongoDB will cancel the query if it exceeds this budget and
|
|
1462
1462
|
# the driver error is translated to {Parse::MongoDB::ExecutionTimeout}.
|
|
1463
|
-
# Pass
|
|
1463
|
+
# Pass `nil` (the default) for no cap.
|
|
1464
1464
|
# @return [Array<Hash>] the raw results from MongoDB
|
|
1465
1465
|
# @param rewrite_lookups [Boolean, nil] when true (default `nil` --
|
|
1466
1466
|
# reads `Parse.rewrite_lookups`), auto-rewrite LLM-style $lookup
|
|
@@ -1469,16 +1469,16 @@ module Parse
|
|
|
1469
1469
|
# @param allow_internal_fields [Boolean] when true, skip the
|
|
1470
1470
|
# internal-fields denylist check (e.g. for SDK-generated ACL
|
|
1471
1471
|
# filters produced by {Parse::Query#readable_by_role} and friends
|
|
1472
|
-
# that legitimately reference
|
|
1473
|
-
# DENIED_OPERATORS walk, forensic-operator-in
|
|
1474
|
-
# internal-field
|
|
1475
|
-
# Passed
|
|
1472
|
+
# that legitimately reference `_rperm`/`_wperm`). The
|
|
1473
|
+
# DENIED_OPERATORS walk, forensic-operator-in-`$expr` check, and
|
|
1474
|
+
# internal-field `$`-reference string check all still run.
|
|
1475
|
+
# Passed `true` only from the SDK direct-execution sites that
|
|
1476
1476
|
# build their pipeline entirely from {Parse::Query#compile_where}:
|
|
1477
|
-
#
|
|
1478
|
-
#
|
|
1479
|
-
#
|
|
1480
|
-
# paths. The Agent MCP tool path and
|
|
1481
|
-
# keep the default
|
|
1477
|
+
# `Parse::Query#results_direct`, `#first_direct` (via
|
|
1478
|
+
# `results_direct`), `#count_direct`, `#distinct_direct`,
|
|
1479
|
+
# `#atlas_search` builder-block, and the two `#group_by_*` direct
|
|
1480
|
+
# paths. The Agent MCP tool path and `Aggregation#execute_direct!`
|
|
1481
|
+
# keep the default `false` so attacker-controlled or user-supplied
|
|
1482
1482
|
# aggregate stages cannot reach internal columns.
|
|
1483
1483
|
# @param session_token [String, nil] when provided, the SDK
|
|
1484
1484
|
# resolves the token to the requesting user + role subscription
|
|
@@ -1582,7 +1582,7 @@ module Parse
|
|
|
1582
1582
|
# are SDK-generated (not attacker-controlled), so no
|
|
1583
1583
|
# re-validation is needed before they're handed to MongoDB.
|
|
1584
1584
|
if (acl_stage = Parse::ACLScope.match_stage_for(resolution))
|
|
1585
|
-
pipeline =
|
|
1585
|
+
pipeline = prepend_or_fold_acl_match(pipeline, acl_stage)
|
|
1586
1586
|
end
|
|
1587
1587
|
pipeline = Parse::ACLScope.rewrite_pipeline(pipeline, resolution)
|
|
1588
1588
|
|
|
@@ -1673,6 +1673,64 @@ module Parse
|
|
|
1673
1673
|
raise
|
|
1674
1674
|
end
|
|
1675
1675
|
|
|
1676
|
+
# Inject the scoped ACL `$match` at the front of a pipeline — UNLESS
|
|
1677
|
+
# the first stage is `$geoNear`, which MongoDB requires to be
|
|
1678
|
+
# pipeline stage 0. In that case fold the ACL predicate into
|
|
1679
|
+
# `$geoNear.query` (a candidate-document pre-filter, semantically
|
|
1680
|
+
# equivalent to a leading `$match`) so the stage-0 invariant holds
|
|
1681
|
+
# and the scoped geo query still enforces `_rperm`.
|
|
1682
|
+
#
|
|
1683
|
+
# A scoped `geo_near` previously failed CLOSED here: the prepended
|
|
1684
|
+
# `$match` pushed `$geoNear` off stage 0 and MongoDB rejected the
|
|
1685
|
+
# whole pipeline. Post-fetch redaction (protectedFields / sub-doc /
|
|
1686
|
+
# internal-field) runs regardless, so folding loses no enforcement.
|
|
1687
|
+
#
|
|
1688
|
+
# @param pipeline [Array<Hash>]
|
|
1689
|
+
# @param acl_stage [Hash] `{ "$match" => <predicate> }` from
|
|
1690
|
+
# {Parse::ACLScope.match_stage_for}.
|
|
1691
|
+
# @return [Array<Hash>] a new pipeline; caller stages are not mutated.
|
|
1692
|
+
def prepend_or_fold_acl_match(pipeline, acl_stage)
|
|
1693
|
+
geo_key = geo_near_stage_key(pipeline.first)
|
|
1694
|
+
return [acl_stage] + pipeline unless geo_key
|
|
1695
|
+
|
|
1696
|
+
match_pred = acl_stage["$match"] || acl_stage[:$match]
|
|
1697
|
+
geo = pipeline.first[geo_key].dup
|
|
1698
|
+
# Match the stage's own key style: reuse an existing `query` key of
|
|
1699
|
+
# either type, else follow the `$geoNear` key's type (string stage
|
|
1700
|
+
# → "query", symbol stage → :query). The Mongo driver normalizes
|
|
1701
|
+
# either way, but keeping one style avoids a duplicate query key.
|
|
1702
|
+
q_key =
|
|
1703
|
+
if geo.key?("query") then "query"
|
|
1704
|
+
elsif geo.key?(:query) then :query
|
|
1705
|
+
elsif geo_key.is_a?(String) then "query"
|
|
1706
|
+
else :query
|
|
1707
|
+
end
|
|
1708
|
+
existing = geo[q_key]
|
|
1709
|
+
geo[q_key] =
|
|
1710
|
+
if existing.is_a?(Hash) && !existing.empty?
|
|
1711
|
+
# `existing` is still the caller's own `$geoNear.query` hash
|
|
1712
|
+
# (the outer `.dup` above is shallow). Embed a copy, not the
|
|
1713
|
+
# original, so the folded pipeline and the caller's pipeline
|
|
1714
|
+
# don't share a mutable hash — honoring the "caller stages are
|
|
1715
|
+
# not mutated" contract in both directions.
|
|
1716
|
+
{ "$and" => [existing.dup, match_pred] }
|
|
1717
|
+
else
|
|
1718
|
+
match_pred
|
|
1719
|
+
end
|
|
1720
|
+
new_first = pipeline.first.dup
|
|
1721
|
+
new_first[geo_key] = geo
|
|
1722
|
+
[new_first] + pipeline[1..]
|
|
1723
|
+
end
|
|
1724
|
+
|
|
1725
|
+
# @return [Symbol, String, nil] the `$geoNear` key (symbol or string
|
|
1726
|
+
# form) if `stage` is a `$geoNear` stage, else nil.
|
|
1727
|
+
def geo_near_stage_key(stage)
|
|
1728
|
+
return nil unless stage.is_a?(Hash)
|
|
1729
|
+
return :$geoNear if stage.key?(:$geoNear)
|
|
1730
|
+
return "$geoNear" if stage.key?("$geoNear")
|
|
1731
|
+
nil
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1676
1734
|
# Execute a `$geoNear` aggregation against a collection, returning
|
|
1677
1735
|
# documents sorted by proximity to `near` along with their computed
|
|
1678
1736
|
# distance. `$geoNear` is the aggregation-pipeline analogue of
|
|
@@ -2048,10 +2106,10 @@ module Parse
|
|
|
2048
2106
|
end
|
|
2049
2107
|
|
|
2050
2108
|
# Convert a raw MongoDB aggregation row, coercing values (BSON ObjectIds,
|
|
2051
|
-
# dates, nested documents) but preserving all field names including
|
|
2052
|
-
# Unlike {.convert_document_to_parse}, this does NOT rename
|
|
2053
|
-
#
|
|
2054
|
-
# group key (e.g. a pointer string like
|
|
2109
|
+
# dates, nested documents) but preserving all field names including `_id`.
|
|
2110
|
+
# Unlike {.convert_document_to_parse}, this does NOT rename `_id` to
|
|
2111
|
+
# `objectId`, because aggregation `$group` stages reuse `_id` as the
|
|
2112
|
+
# group key (e.g. a pointer string like `"Workspace$abc"`) rather than as a
|
|
2055
2113
|
# Parse object identifier.
|
|
2056
2114
|
#
|
|
2057
2115
|
# @param doc [Hash] a raw MongoDB aggregation result row
|
|
@@ -2379,7 +2437,7 @@ module Parse
|
|
|
2379
2437
|
# DENIED_OPERATORS walk still runs. Intended only for callers
|
|
2380
2438
|
# that built the pipeline via {Parse::Query}'s own constraint
|
|
2381
2439
|
# DSL (e.g. {Parse::Query#readable_by_role}); raw user-supplied
|
|
2382
|
-
# pipelines (Agent MCP tools) must keep the default
|
|
2440
|
+
# pipelines (Agent MCP tools) must keep the default `false`.
|
|
2383
2441
|
#
|
|
2384
2442
|
# Public for testability and for callers that want to validate
|
|
2385
2443
|
# input before forwarding to {.find} / {.aggregate}.
|
|
@@ -282,10 +282,10 @@ module Parse
|
|
|
282
282
|
# @param node [Hash, Array, Object] the structure to walk.
|
|
283
283
|
# @param allow_internal_fields [Boolean] when true, skip the
|
|
284
284
|
# {INTERNAL_FIELDS_DENYLIST} check (e.g. for SDK-generated ACL
|
|
285
|
-
# filters that legitimately reference
|
|
285
|
+
# filters that legitimately reference `_rperm`/`_wperm` via
|
|
286
286
|
# {Parse::Query#readable_by_role} and friends). The
|
|
287
287
|
# {DENIED_OPERATORS} walk and forensic-operator gating still
|
|
288
|
-
# apply. Default
|
|
288
|
+
# apply. Default `false` for callers that forward raw,
|
|
289
289
|
# user-influenced pipelines (e.g. Agent MCP tools).
|
|
290
290
|
# @raise [Error] if a denied operator is found at any depth.
|
|
291
291
|
# @return [true]
|
|
@@ -2550,21 +2550,21 @@ module Parse
|
|
|
2550
2550
|
|
|
2551
2551
|
# @!visibility private
|
|
2552
2552
|
# Shared helpers for the four ACL constraint subclasses
|
|
2553
|
-
# (
|
|
2554
|
-
#
|
|
2553
|
+
# (`:ACL.readable_by`, `:ACL.readable_by_role`, `:ACL.writable_by`,
|
|
2554
|
+
# `:ACL.writable_by_role`). Collects a list of permission strings
|
|
2555
2555
|
# from a caller-supplied value of User/Role/Pointer/Array/String,
|
|
2556
2556
|
# using {Parse::Role.all_for_user} (user inputs) and
|
|
2557
2557
|
# {Parse::Role#all_parent_role_names} (role inputs) so the
|
|
2558
2558
|
# traversal walks the inheritance direction Parse Server actually
|
|
2559
|
-
# enforces. Prior implementations inlined
|
|
2559
|
+
# enforces. Prior implementations inlined `role.all_child_roles`,
|
|
2560
2560
|
# which traverses the wrong direction and over-grants.
|
|
2561
2561
|
module ACLPermissions
|
|
2562
2562
|
module_function
|
|
2563
2563
|
|
|
2564
|
-
# Expand a
|
|
2565
|
-
# permission-string array. Uses explicit
|
|
2566
|
-
# than
|
|
2567
|
-
# overridden
|
|
2564
|
+
# Expand a `:ACL.readable_by` / `:ACL.writable_by` value into a
|
|
2565
|
+
# permission-string array. Uses explicit `is_a?` calls (rather
|
|
2566
|
+
# than `case/when`) so callers that pass duck-typed mocks with
|
|
2567
|
+
# overridden `is_a?` — common in the constraint test suite —
|
|
2568
2568
|
# continue to route correctly.
|
|
2569
2569
|
# @param value [Parse::User, Parse::Role, Parse::Pointer, String, Array]
|
|
2570
2570
|
# @return [Array<String>]
|
|
@@ -2612,9 +2612,9 @@ module Parse
|
|
|
2612
2612
|
str == "public" ? "*" : str
|
|
2613
2613
|
end
|
|
2614
2614
|
|
|
2615
|
-
# Expand a
|
|
2615
|
+
# Expand a `:ACL.readable_by_role` / `:ACL.writable_by_role` value
|
|
2616
2616
|
# into a permission-string array. Differs from {.collect} by
|
|
2617
|
-
# auto-prefixing bare strings with
|
|
2617
|
+
# auto-prefixing bare strings with `"role:"` and refusing
|
|
2618
2618
|
# non-role arguments.
|
|
2619
2619
|
# @param value [Parse::Role, Parse::Pointer, String, Array]
|
|
2620
2620
|
# @return [Array<String>]
|
|
@@ -2686,18 +2686,18 @@ module Parse
|
|
|
2686
2686
|
|
|
2687
2687
|
# Compile a final permission-string array into an aggregation
|
|
2688
2688
|
# pipeline match-stage on the requested permission field. The
|
|
2689
|
-
#
|
|
2690
|
-
# / {Parse::ACL.write_predicate} so a missing
|
|
2689
|
+
# `$exists: false` branch is appended by {Parse::ACL.read_predicate}
|
|
2690
|
+
# / {Parse::ACL.write_predicate} so a missing `_rperm`/`_wperm`
|
|
2691
2691
|
# (treated as public by Parse Server) still matches.
|
|
2692
2692
|
# @param permissions [Array<String>]
|
|
2693
|
-
# @param field [String]
|
|
2693
|
+
# @param field [String] `"_rperm"` or `"_wperm"`.
|
|
2694
2694
|
# @return [Hash] aggregation-pipeline wrapper compatible with
|
|
2695
2695
|
# {Parse::Query}'s constraint-build contract.
|
|
2696
2696
|
# @param strict [Boolean] when true, build an EXACT match: suppress
|
|
2697
|
-
# both the implicit public
|
|
2698
|
-
# (
|
|
2699
|
-
# literally contains one of
|
|
2700
|
-
#
|
|
2697
|
+
# both the implicit public `"*"` grant AND the missing-field
|
|
2698
|
+
# (`$exists: false`) branch, so only rows whose `_rperm`/`_wperm`
|
|
2699
|
+
# literally contains one of `permissions` match. Used by the
|
|
2700
|
+
# `readable_by(..., strict: true)` / `readable_by_exact` surface.
|
|
2701
2701
|
def pipeline(permissions, field:, strict: false)
|
|
2702
2702
|
deduped = permissions.compact.reject(&:empty?).uniq
|
|
2703
2703
|
if deduped.empty?
|
|
@@ -2712,9 +2712,9 @@ module Parse
|
|
|
2712
2712
|
end
|
|
2713
2713
|
|
|
2714
2714
|
# @!visibility private
|
|
2715
|
-
# Whether a
|
|
2716
|
-
# permissions" (master-key-only):
|
|
2717
|
-
#
|
|
2715
|
+
# Whether a `readable_by` / `writable_by` value expresses "no
|
|
2716
|
+
# permissions" (master-key-only): `nil`, an empty Array, the String
|
|
2717
|
+
# `"none"`, or the Symbol `:none`. These map to {.empty_pipeline}.
|
|
2718
2718
|
def empty_intent?(value)
|
|
2719
2719
|
return true if value.nil?
|
|
2720
2720
|
return true if value == "none" || value == :none
|
|
@@ -2724,11 +2724,11 @@ module Parse
|
|
|
2724
2724
|
|
|
2725
2725
|
# @!visibility private
|
|
2726
2726
|
# The match for "no permissions": an explicit empty array. A missing
|
|
2727
|
-
#
|
|
2728
|
-
# opposite of master-only — so it must NOT match here.
|
|
2727
|
+
# `_rperm`/`_wperm` is treated by Parse Server as PUBLIC — the
|
|
2728
|
+
# opposite of master-only — so it must NOT match here. `$eq: []`
|
|
2729
2729
|
# already excludes a missing field (missing != []); the +$exists:
|
|
2730
2730
|
# true+ guard documents that intent.
|
|
2731
|
-
# @param field [String]
|
|
2731
|
+
# @param field [String] `"_rperm"` or `"_wperm"`.
|
|
2732
2732
|
def empty_pipeline(field:)
|
|
2733
2733
|
{ "__aggregation_pipeline" => [
|
|
2734
2734
|
{ "$match" => { field => { "$exists" => true, "$eq" => [] } } },
|
|
@@ -2736,11 +2736,11 @@ module Parse
|
|
|
2736
2736
|
end
|
|
2737
2737
|
|
|
2738
2738
|
# @!visibility private
|
|
2739
|
-
# Permission keys for a
|
|
2739
|
+
# Permission keys for a `not_readable_by` / `not_writable_by` value:
|
|
2740
2740
|
# the expanded grant set (user→roles, role→parent roles) PLUS the
|
|
2741
|
-
# public
|
|
2741
|
+
# public `"*"` wildcard. A public row is readable/writable by everyone,
|
|
2742
2742
|
# so it must be EXCLUDED from a "not readable/writable by X" result —
|
|
2743
|
-
# hence
|
|
2743
|
+
# hence `"*"` is added to the `$nin` set. Returns `[]` for an
|
|
2744
2744
|
# empty-intent value (no negation constraint is applied).
|
|
2745
2745
|
# @return [Array<String>]
|
|
2746
2746
|
def collect_for_negation(value)
|
|
@@ -2831,7 +2831,7 @@ module Parse
|
|
|
2831
2831
|
register :readable_by
|
|
2832
2832
|
|
|
2833
2833
|
# @return [Boolean] whether to compile an EXACT match (suppress the
|
|
2834
|
-
# implicit public
|
|
2834
|
+
# implicit public `"*"` grant and the missing-field branch).
|
|
2835
2835
|
# Overridden by {ACLReadableByExactConstraint}.
|
|
2836
2836
|
def strict?
|
|
2837
2837
|
false
|
|
@@ -2855,10 +2855,10 @@ module Parse
|
|
|
2855
2855
|
end
|
|
2856
2856
|
|
|
2857
2857
|
# Strict variant of {ACLReadableByConstraint}: matches ONLY rows whose
|
|
2858
|
-
#
|
|
2859
|
-
# implicit public
|
|
2860
|
-
# Reached via
|
|
2861
|
-
#
|
|
2858
|
+
# `_rperm` literally contains one of the resolved permissions — no
|
|
2859
|
+
# implicit public `"*"` and no missing-`_rperm` (public-by-absence) rows.
|
|
2860
|
+
# Reached via `Query#readable_by(value, strict: true)` or the
|
|
2861
|
+
# `:ACL.readable_by_exact` symbol operator. Use this for ownership /
|
|
2862
2862
|
# security audits ("which rows explicitly grant this principal") rather
|
|
2863
2863
|
# than access simulation ("what can this principal read").
|
|
2864
2864
|
class ACLReadableByExactConstraint < ACLReadableByConstraint
|
|
@@ -3185,23 +3185,23 @@ module Parse
|
|
|
3185
3185
|
end
|
|
3186
3186
|
end
|
|
3187
3187
|
|
|
3188
|
-
# @deprecated Thin alias of {ACLReadableByConstraint}. The
|
|
3188
|
+
# @deprecated Thin alias of {ACLReadableByConstraint}. The `:readable_by`
|
|
3189
3189
|
# operator is registered by {ACLReadableByConstraint}; this constant is
|
|
3190
3190
|
# retained only so any code referencing it keeps working. The previous
|
|
3191
3191
|
# standalone implementation (no role expansion, no implicit public
|
|
3192
|
-
#
|
|
3193
|
-
# the
|
|
3192
|
+
# `"*"`, divergent empty-ACL shape) has been removed — it never backed
|
|
3193
|
+
# the `:readable_by` operator and silently disagreed with it.
|
|
3194
3194
|
class ReadableByConstraint < ACLReadableByConstraint
|
|
3195
3195
|
end
|
|
3196
3196
|
|
|
3197
3197
|
# @deprecated Alias of {ACLWritableByConstraint}. The British-spelled
|
|
3198
|
-
#
|
|
3199
|
-
# role-expanding implementation as
|
|
3198
|
+
# `:writeable_by` operator now resolves to the SAME public-inclusive,
|
|
3199
|
+
# role-expanding implementation as `:writable_by` — previously it was a
|
|
3200
3200
|
# separate, strict, non-expanding constraint, so the one-letter spelling
|
|
3201
3201
|
# difference silently changed query semantics. For the old exact-match
|
|
3202
3202
|
# behavior (no implicit public, no role expansion, no missing-field),
|
|
3203
|
-
# use
|
|
3204
|
-
# or the
|
|
3203
|
+
# use `readable_by(..., strict: true)` / `writable_by(..., strict: true)`
|
|
3204
|
+
# or the `:writable_by_exact` operator.
|
|
3205
3205
|
class WriteableByConstraint < ACLWritableByConstraint
|
|
3206
3206
|
register :writeable_by
|
|
3207
3207
|
end
|
|
@@ -3223,7 +3223,7 @@ module Parse
|
|
|
3223
3223
|
#
|
|
3224
3224
|
# @note "Not readable by X" excludes rows readable by X *directly*, *via
|
|
3225
3225
|
# any role X inherits*, AND *publicly* — so a User value expands its
|
|
3226
|
-
# roles and the public
|
|
3226
|
+
# roles and the public `"*"` is always added to the exclusion set.
|
|
3227
3227
|
# @note This constraint uses aggregation pipeline because Parse Server
|
|
3228
3228
|
# restricts direct queries on the internal _rperm field.
|
|
3229
3229
|
class NotReadableByConstraint < Constraint
|