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/atlas_search.rb
CHANGED
|
@@ -37,21 +37,21 @@ module Parse
|
|
|
37
37
|
# Error raised for invalid search parameters
|
|
38
38
|
class InvalidSearchParameters < StandardError; end
|
|
39
39
|
|
|
40
|
-
# Error raised when the caller did not supply
|
|
41
|
-
#
|
|
40
|
+
# Error raised when the caller did not supply `session_token:` or
|
|
41
|
+
# `master: true` and {.require_session_token} is `true`. Atlas
|
|
42
42
|
# Search bypasses Parse Server's ACL evaluation, so the caller
|
|
43
43
|
# must either pass a session token (so the SDK can inject a
|
|
44
|
-
#
|
|
44
|
+
# `_rperm` `$match`) or explicitly opt into master-key semantics.
|
|
45
45
|
class ACLRequired < StandardError; end
|
|
46
46
|
|
|
47
|
-
# Error raised when {.faceted_search} is called with a
|
|
48
|
-
#
|
|
47
|
+
# Error raised when {.faceted_search} is called with a `session_token`.
|
|
48
|
+
# `$searchMeta` returns a single metadata document — bucket
|
|
49
49
|
# counts that include restricted documents and cannot be
|
|
50
|
-
# post-filtered with
|
|
50
|
+
# post-filtered with `$match` because the matched documents are
|
|
51
51
|
# not in the output stream. ACL-safe faceting requires the search
|
|
52
|
-
# index to tokenize
|
|
52
|
+
# index to tokenize `_rperm` and a `compound.filter` injection
|
|
53
53
|
# path; both are deferred to a follow-up release. Callers that
|
|
54
|
-
# need ACL-aware faceting today must either run with
|
|
54
|
+
# need ACL-aware faceting today must either run with `master: true`
|
|
55
55
|
# or implement post-aggregation filtering themselves.
|
|
56
56
|
class FacetedSearchNotACLSafe < StandardError; end
|
|
57
57
|
|
|
@@ -79,23 +79,23 @@ module Parse
|
|
|
79
79
|
attr_accessor :allow_raw
|
|
80
80
|
|
|
81
81
|
# @!attribute [rw] require_session_token
|
|
82
|
-
# When
|
|
82
|
+
# When `true`, {.search}, {.autocomplete}, and
|
|
83
83
|
# {.faceted_search} raise {ACLRequired} unless the caller
|
|
84
|
-
# passes either
|
|
85
|
-
#
|
|
86
|
-
#
|
|
84
|
+
# passes either `session_token:` or `master: true`. Default:
|
|
85
|
+
# `false`, matching the pre-ACL behavior — a one-time
|
|
86
|
+
# `[Parse::AtlasSearch:SECURITY]` banner is emitted instead
|
|
87
87
|
# for missing-token calls, the same pattern used by
|
|
88
88
|
# {Parse::Agent} for master-key construction.
|
|
89
89
|
#
|
|
90
90
|
# New deployments are strongly encouraged to flip this to
|
|
91
|
-
#
|
|
91
|
+
# `true` at startup. The next major release will flip the
|
|
92
92
|
# default.
|
|
93
93
|
# @return [Boolean]
|
|
94
94
|
attr_accessor :require_session_token
|
|
95
95
|
|
|
96
96
|
# @!attribute [rw] session_cache_ttl
|
|
97
97
|
# TTL (seconds) for {Session}'s session-token → user-id cache.
|
|
98
|
-
# Default: 3600 (1 hour). Longer values reduce
|
|
98
|
+
# Default: 3600 (1 hour). Longer values reduce `/users/me`
|
|
99
99
|
# round-trips but extend the window during which a revoked
|
|
100
100
|
# session can still authenticate Atlas Search calls; apps
|
|
101
101
|
# with sub-TTL revocation requirements should call
|
|
@@ -115,8 +115,8 @@ module Parse
|
|
|
115
115
|
# @!attribute [rw] session_cache
|
|
116
116
|
# Pluggable cache for {Session}'s session-token lookups.
|
|
117
117
|
# Replace with a Redis/Memcached adapter for cross-process
|
|
118
|
-
# sharing; the object must respond to
|
|
119
|
-
#
|
|
118
|
+
# sharing; the object must respond to `get(key)`,
|
|
119
|
+
# `set(key, value, ttl:)`, and `invalidate(key)`. Defaults
|
|
120
120
|
# to a process-local {Session::MemoryCache}.
|
|
121
121
|
# @return [#get, #set, #invalidate]
|
|
122
122
|
attr_accessor :session_cache
|
|
@@ -135,9 +135,9 @@ module Parse
|
|
|
135
135
|
# (raw flag ignored) in production-like environments and
|
|
136
136
|
# `true` when RACK_ENV/RAILS_ENV is `development` or `test`.
|
|
137
137
|
# Internal-field stripping runs regardless.
|
|
138
|
-
# @param require_session_token [Boolean] when
|
|
139
|
-
# calls without
|
|
140
|
-
# {ACLRequired}. See {#require_session_token}. Default:
|
|
138
|
+
# @param require_session_token [Boolean] when `true`, library
|
|
139
|
+
# calls without `session_token:` or `master: true` raise
|
|
140
|
+
# {ACLRequired}. See {#require_session_token}. Default: `false`.
|
|
141
141
|
# @param session_cache_ttl [Integer] session-token cache TTL
|
|
142
142
|
# (seconds). Default: 3600.
|
|
143
143
|
# @param role_cache_ttl [Integer] role-name cache TTL (seconds).
|
|
@@ -252,7 +252,7 @@ module Parse
|
|
|
252
252
|
# @option options [String, Parse::Role] :acl_role act as the given role for
|
|
253
253
|
# ACL evaluation (no REST equivalent; mongo-direct only).
|
|
254
254
|
# @option options [Symbol] :read_preference MongoDB read preference applied
|
|
255
|
-
# to the underlying collection (e.g.
|
|
255
|
+
# to the underlying collection (e.g. `:secondary`).
|
|
256
256
|
# @option options [Integer] :max_time_ms maximum server-side execution time
|
|
257
257
|
# in milliseconds for the aggregate command.
|
|
258
258
|
#
|
|
@@ -316,80 +316,77 @@ module Parse
|
|
|
316
316
|
builder.with_highlight(path: options[:highlight_field])
|
|
317
317
|
end
|
|
318
318
|
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
unless resolution.master?
|
|
344
|
-
acl_match = Parse::ACLScope.match_stage_for(resolution)
|
|
345
|
-
pipeline << acl_match if acl_match
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# Add filter stage if provided
|
|
349
|
-
if options[:filter]
|
|
350
|
-
mongo_filter = convert_filter_for_mongodb(options[:filter], collection_name)
|
|
351
|
-
pipeline << { "$match" => mongo_filter }
|
|
352
|
-
end
|
|
319
|
+
# $search MUST be stage 0 of an Atlas Search pipeline (MongoDB
|
|
320
|
+
# rejects any other first stage), so we must NOT route through
|
|
321
|
+
# Parse::MongoDB.aggregate — that helper prepends the ACL $match
|
|
322
|
+
# to position 0. Execution plus the full SDK-side enforcement
|
|
323
|
+
# chain (ACL $match placed AFTER $search, protectedFields strip,
|
|
324
|
+
# pointerFields filter, sub-doc redaction) lives in
|
|
325
|
+
# `search_pipeline!`, shared with the builder-block path
|
|
326
|
+
# (`search_with_stage` / Parse::Query#atlas_search).
|
|
327
|
+
search_pipeline!(
|
|
328
|
+
collection_name, builder.build,
|
|
329
|
+
resolution: resolution,
|
|
330
|
+
protected_fields: protected_fields,
|
|
331
|
+
pointer_fields: pointer_fields,
|
|
332
|
+
highlight_field: options[:highlight_field],
|
|
333
|
+
filter: options[:filter],
|
|
334
|
+
sort: options[:sort],
|
|
335
|
+
skip: skip_val,
|
|
336
|
+
limit: limit,
|
|
337
|
+
max_time_ms: options[:max_time_ms],
|
|
338
|
+
read_preference: read_preference,
|
|
339
|
+
class_name: options[:class_name] || collection_name,
|
|
340
|
+
raw: options[:raw],
|
|
341
|
+
)
|
|
342
|
+
end
|
|
353
343
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
344
|
+
# Execute a caller-supplied `$search` stage (built via a
|
|
345
|
+
# {SearchBuilder}) with the same ACL/CLP/protectedFields/
|
|
346
|
+
# pointerFields enforcement as {.search}, keeping `$search` at
|
|
347
|
+
# stage 0. This is the entry point for Parse::Query#atlas_search's
|
|
348
|
+
# builder-block mode, which constructs its own `$search` stage
|
|
349
|
+
# instead of going through the query/fields/fuzzy options.
|
|
350
|
+
#
|
|
351
|
+
# Auth is resolved from `options` exactly like {.search}:
|
|
352
|
+
# `session_token:` / `master: true` / `acl_user:` / `acl_role:`
|
|
353
|
+
# (mutually exclusive). Without one, behavior follows
|
|
354
|
+
# `require_session_token` (public-only fallback, or `ACLRequired`).
|
|
355
|
+
#
|
|
356
|
+
# @param collection_name [String] the Parse collection name
|
|
357
|
+
# @param search_stage [Hash] a built `{ "$search" => {...} }` stage
|
|
358
|
+
# @param options [Hash] `:filter`, `:sort`, `:skip`, `:limit`,
|
|
359
|
+
# `:highlight_field`, `:max_time_ms`, `:read_preference`,
|
|
360
|
+
# `:class_name`, `:raw`, plus the auth kwargs above.
|
|
361
|
+
# @return [Parse::AtlasSearch::SearchResult]
|
|
362
|
+
def search_with_stage(collection_name, search_stage, **options)
|
|
363
|
+
require_available!
|
|
364
|
+
assert_search_stage_safe!(search_stage)
|
|
357
365
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
366
|
+
read_preference = options.delete(:read_preference)
|
|
367
|
+
resolution = resolve_scope!(options, method_name: :search)
|
|
368
|
+
assert_clp_find!(collection_name, resolution)
|
|
369
|
+
pointer_fields = resolve_pointer_fields!(collection_name, resolution)
|
|
370
|
+
protected_fields = Parse::CLPScope.protected_fields_for(
|
|
371
|
+
collection_name, resolution.permission_strings,
|
|
372
|
+
)
|
|
373
|
+
assert_highlight_field_allowed!(options[:highlight_field], protected_fields, resolution)
|
|
361
374
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
375
|
+
search_pipeline!(
|
|
376
|
+
collection_name, search_stage,
|
|
377
|
+
resolution: resolution,
|
|
378
|
+
protected_fields: protected_fields,
|
|
379
|
+
pointer_fields: pointer_fields,
|
|
380
|
+
highlight_field: options[:highlight_field],
|
|
381
|
+
filter: options[:filter],
|
|
382
|
+
sort: options[:sort],
|
|
383
|
+
skip: options[:skip] || 0,
|
|
384
|
+
limit: options[:limit] || 100,
|
|
385
|
+
max_time_ms: options[:max_time_ms],
|
|
369
386
|
read_preference: read_preference,
|
|
387
|
+
class_name: options[:class_name] || collection_name,
|
|
388
|
+
raw: options[:raw],
|
|
370
389
|
)
|
|
371
|
-
|
|
372
|
-
# Post-fetch enforcement: walk the result rows the same way
|
|
373
|
-
# Parse::MongoDB.aggregate would. Master mode is the ACL bypass
|
|
374
|
-
# — skip every redaction layer (matches the helper's behavior).
|
|
375
|
-
unless resolution.master?
|
|
376
|
-
Parse::ACLScope.redact_results!(raw_results, resolution)
|
|
377
|
-
Parse::CLPScope.redact_protected_fields!(raw_results, protected_fields) if protected_fields.any?
|
|
378
|
-
if pointer_fields
|
|
379
|
-
raw_results = Parse::CLPScope.filter_by_pointer_fields(
|
|
380
|
-
raw_results, pointer_fields, resolution.user_id,
|
|
381
|
-
)
|
|
382
|
-
end
|
|
383
|
-
# ATLAS-4: drop any `_highlights` entry whose `path` names a
|
|
384
|
-
# protected field. `searchHighlights` returns the matched
|
|
385
|
-
# token plus its surrounding text, which would otherwise leak
|
|
386
|
-
# the protected field's value through the snippet.
|
|
387
|
-
strip_protected_highlights!(raw_results, protected_fields) if protected_fields.any?
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Convert results
|
|
391
|
-
class_name = options[:class_name] || collection_name
|
|
392
|
-
process_search_results(raw_results, class_name, options[:raw])
|
|
393
390
|
end
|
|
394
391
|
|
|
395
392
|
# Perform an autocomplete search for search-as-you-type functionality.
|
|
@@ -415,7 +412,7 @@ module Parse
|
|
|
415
412
|
# @option options [String, Parse::Role] :acl_role act as the given role for
|
|
416
413
|
# ACL evaluation (no REST equivalent; mongo-direct only).
|
|
417
414
|
# @option options [Symbol] :read_preference MongoDB read preference applied
|
|
418
|
-
# to the underlying collection (e.g.
|
|
415
|
+
# to the underlying collection (e.g. `:secondary`).
|
|
419
416
|
# @option options [Integer] :max_time_ms maximum server-side execution time
|
|
420
417
|
# in milliseconds for the aggregate command.
|
|
421
418
|
#
|
|
@@ -489,7 +486,7 @@ module Parse
|
|
|
489
486
|
|
|
490
487
|
# Add filter if provided
|
|
491
488
|
if options[:filter]
|
|
492
|
-
mongo_filter = convert_filter_for_mongodb(options[:filter], collection_name)
|
|
489
|
+
mongo_filter = convert_filter_for_mongodb(options[:filter], collection_name, resolution: resolution)
|
|
493
490
|
pipeline << { "$match" => mongo_filter }
|
|
494
491
|
end
|
|
495
492
|
|
|
@@ -535,11 +532,11 @@ module Parse
|
|
|
535
532
|
# @param query [String, nil] the search query text (nil for match-all)
|
|
536
533
|
# @param facets [Hash] facet definitions
|
|
537
534
|
# @param options [Hash] search options (same as {#search}; see that
|
|
538
|
-
# method for the full list of accepted
|
|
539
|
-
#
|
|
540
|
-
#
|
|
541
|
-
#
|
|
542
|
-
#
|
|
535
|
+
# method for the full list of accepted `@option` entries including
|
|
536
|
+
# `:index`, `:fields`, `:fuzzy`, `:limit`, `:filter`, `:read_preference`,
|
|
537
|
+
# `:max_time_ms`, and the scoping kwargs `:master`, `:session_token`,
|
|
538
|
+
# `:acl_user`, `:acl_role`). Note: scoped identity kwargs require
|
|
539
|
+
# `master: true` to be passed explicitly — $searchMeta bucket counts
|
|
543
540
|
# cannot be filtered by ACL after the fact, so the method refuses
|
|
544
541
|
# to silently downgrade.
|
|
545
542
|
#
|
|
@@ -681,6 +678,85 @@ module Parse
|
|
|
681
678
|
|
|
682
679
|
private
|
|
683
680
|
|
|
681
|
+
# Execute a `$search`-first pipeline with the full SDK-side
|
|
682
|
+
# ACL/CLP enforcement chain, keeping `$search` at stage 0. Shared
|
|
683
|
+
# by {.search} (options API) and {.search_with_stage} (the
|
|
684
|
+
# builder-block path used by Parse::Query#atlas_search). The
|
|
685
|
+
# caller must have already resolved scope and pre-flighted the CLP
|
|
686
|
+
# `find` / pointerFields / protectedFields / highlight-field
|
|
687
|
+
# checks.
|
|
688
|
+
def search_pipeline!(collection_name, search_stage, resolution:,
|
|
689
|
+
protected_fields:, pointer_fields:,
|
|
690
|
+
highlight_field: nil, filter: nil, sort: nil,
|
|
691
|
+
skip: 0, limit: 100, max_time_ms: nil,
|
|
692
|
+
read_preference: nil, class_name: nil, raw: false)
|
|
693
|
+
# Backstop the stage-safety check at the shared execution chokepoint
|
|
694
|
+
# so ANY path that runs a $search stage (not just search_with_stage)
|
|
695
|
+
# rejects a non-$search stage / returnStoredSource. The .search and
|
|
696
|
+
# .autocomplete callers build safe stages via SearchBuilder, so this
|
|
697
|
+
# only ever fires for a caller-supplied stage.
|
|
698
|
+
assert_search_stage_safe!(search_stage)
|
|
699
|
+
pipeline = [search_stage]
|
|
700
|
+
|
|
701
|
+
# Score projection.
|
|
702
|
+
pipeline << { "$addFields" => { "_score" => { "$meta" => "searchScore" } } }
|
|
703
|
+
|
|
704
|
+
# Highlights projection if requested.
|
|
705
|
+
if highlight_field
|
|
706
|
+
pipeline << { "$addFields" => { "_highlights" => { "$meta" => "searchHighlights" } } }
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Inject ACL $match AFTER $search / $addFields but BEFORE the
|
|
710
|
+
# caller-supplied filter, so a user-controlled filter cannot
|
|
711
|
+
# exfiltrate restricted documents that passed the $search
|
|
712
|
+
# operator. The $exists: false branch in `read_predicate` covers
|
|
713
|
+
# documents Parse Server treats as public (no _rperm). Master
|
|
714
|
+
# mode is the ACL bypass — skip it (matches
|
|
715
|
+
# Parse::MongoDB.aggregate's behavior).
|
|
716
|
+
unless resolution.master?
|
|
717
|
+
acl_match = Parse::ACLScope.match_stage_for(resolution)
|
|
718
|
+
pipeline << acl_match if acl_match
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Caller-supplied filter, sanitized against operator injection
|
|
722
|
+
# AND protected-field oracle probing (via the resolution).
|
|
723
|
+
if filter
|
|
724
|
+
mongo_filter = convert_filter_for_mongodb(filter, collection_name, resolution: resolution)
|
|
725
|
+
pipeline << { "$match" => mongo_filter }
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
pipeline << { "$sort" => (sort || { "_score" => -1 }) }
|
|
729
|
+
pipeline << { "$skip" => skip } if skip.to_i > 0
|
|
730
|
+
pipeline << { "$limit" => limit }
|
|
731
|
+
|
|
732
|
+
# Execute directly against the collection — bypasses
|
|
733
|
+
# Parse::MongoDB.aggregate so its ACL-prepend doesn't violate the
|
|
734
|
+
# $search-at-stage-0 invariant. We reproduce the SDK-side
|
|
735
|
+
# enforcement chain inline below.
|
|
736
|
+
raw_results = run_atlas_pipeline!(
|
|
737
|
+
collection_name, pipeline, max_time_ms, read_preference: read_preference,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Post-fetch enforcement: walk the result rows the same way
|
|
741
|
+
# Parse::MongoDB.aggregate would. Master mode skips every
|
|
742
|
+
# redaction layer.
|
|
743
|
+
unless resolution.master?
|
|
744
|
+
Parse::ACLScope.redact_results!(raw_results, resolution)
|
|
745
|
+
Parse::CLPScope.redact_protected_fields!(raw_results, protected_fields) if protected_fields.any?
|
|
746
|
+
if pointer_fields
|
|
747
|
+
raw_results = Parse::CLPScope.filter_by_pointer_fields(
|
|
748
|
+
raw_results, pointer_fields, resolution.user_id,
|
|
749
|
+
)
|
|
750
|
+
end
|
|
751
|
+
# ATLAS-4: drop any `_highlights` entry whose `path` names a
|
|
752
|
+
# protected field — the snippet would otherwise leak the
|
|
753
|
+
# protected value.
|
|
754
|
+
strip_protected_highlights!(raw_results, protected_fields) if protected_fields.any?
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
process_search_results(raw_results, class_name || collection_name, raw)
|
|
758
|
+
end
|
|
759
|
+
|
|
684
760
|
def require_available!
|
|
685
761
|
Parse::MongoDB.require_gem!
|
|
686
762
|
unless available?
|
|
@@ -690,30 +766,30 @@ module Parse
|
|
|
690
766
|
end
|
|
691
767
|
end
|
|
692
768
|
|
|
693
|
-
# Pop the auth-related kwargs (
|
|
694
|
-
#
|
|
769
|
+
# Pop the auth-related kwargs (`:session_token`, `:master`,
|
|
770
|
+
# `:acl_user`, `:acl_role`) off `options` and return a fully
|
|
695
771
|
# resolved {Parse::ACLScope::Resolution}. Replaces the old
|
|
696
|
-
#
|
|
772
|
+
# `resolve_acl_options!` shim that returned a bare Hash — the
|
|
697
773
|
# post-fetch enforcement chain ({Parse::ACLScope.redact_results!},
|
|
698
774
|
# {Parse::CLPScope.redact_protected_fields!}, etc.) all consume a
|
|
699
775
|
# Resolution, so producing one here keeps the call sites uniform.
|
|
700
776
|
#
|
|
701
777
|
# Modes match {Parse::ACLScope::Resolution}:
|
|
702
778
|
#
|
|
703
|
-
# *
|
|
704
|
-
#
|
|
779
|
+
# * `:session` — `session_token:` resolved, or `acl_user:` /
|
|
780
|
+
# `acl_role:` supplied. ACL+CLP+protectedFields enforcement
|
|
705
781
|
# runs in full.
|
|
706
|
-
# *
|
|
782
|
+
# * `:master` — `master: true`. ACL/CLP enforcement is bypassed
|
|
707
783
|
# (the caller has explicit master-key intent).
|
|
708
|
-
# *
|
|
709
|
-
# is
|
|
784
|
+
# * `:public` — no scope kwargs supplied, `require_session_token`
|
|
785
|
+
# is `false`. A one-time banner is emitted and the call
|
|
710
786
|
# falls through with public-only ACL semantics — public-mode
|
|
711
787
|
# enforcement still runs (refused rows are filtered, the
|
|
712
788
|
# CLP allowlist is consulted), the perms set is just
|
|
713
|
-
#
|
|
789
|
+
# `["*"]` rather than user-scoped.
|
|
714
790
|
#
|
|
715
791
|
# Raises {ACLRequired} when no scope kwargs are supplied and
|
|
716
|
-
# {.require_session_token} is
|
|
792
|
+
# {.require_session_token} is `true`. The agent-tool path
|
|
717
793
|
# refuses unconditionally regardless of this toggle — see
|
|
718
794
|
# {Parse::Agent::Tools}.
|
|
719
795
|
def resolve_scope!(options, method_name:)
|
|
@@ -880,9 +956,9 @@ module Parse
|
|
|
880
956
|
raise
|
|
881
957
|
end
|
|
882
958
|
|
|
883
|
-
# Emit a one-time
|
|
959
|
+
# Emit a one-time `[Parse::AtlasSearch:SECURITY]` banner the
|
|
884
960
|
# first time an Atlas Search call runs without a session_token
|
|
885
|
-
# and without an explicit
|
|
961
|
+
# and without an explicit `master: true`. Mirrors the
|
|
886
962
|
# warned-once pattern {Parse::Agent} uses for master-key
|
|
887
963
|
# construction so noisy logs don't drown out the warning, but
|
|
888
964
|
# one log line per process is enough to surface the misuse to
|
|
@@ -909,14 +985,71 @@ module Parse
|
|
|
909
985
|
Array(fields).map(&:to_s)
|
|
910
986
|
end
|
|
911
987
|
|
|
912
|
-
|
|
988
|
+
# Validate a caller-supplied `$search` stage before it is executed
|
|
989
|
+
# by {.search_with_stage}. The SDK's own {SearchBuilder} always emits
|
|
990
|
+
# a safe stage, but `search_with_stage` is public and a caller can
|
|
991
|
+
# hand-roll one — so we refuse the shapes that would silently defeat
|
|
992
|
+
# ACL enforcement.
|
|
993
|
+
#
|
|
994
|
+
# The load-bearing check is `returnStoredSource`: with stored-source
|
|
995
|
+
# projection Atlas returns ONLY the index-stored fields. If `_rperm`
|
|
996
|
+
# is not among them, the post-`$search` ACL `$match` (and the
|
|
997
|
+
# post-fetch redaction) treat the row as public — `_rperm` absent is
|
|
998
|
+
# interpreted as "no restriction" — so an ACL-restricted document's
|
|
999
|
+
# stored fields leak. There is no per-document rehydration on this
|
|
1000
|
+
# path, so the only safe answer is to reject the flag outright.
|
|
1001
|
+
#
|
|
1002
|
+
# @param search_stage [Hash] the `{ "$search" => {...} }` stage.
|
|
1003
|
+
# @raise [InvalidSearchParameters] on a non-$search or unsafe stage.
|
|
1004
|
+
def assert_search_stage_safe!(search_stage)
|
|
1005
|
+
unless search_stage.is_a?(Hash)
|
|
1006
|
+
raise InvalidSearchParameters,
|
|
1007
|
+
"search_stage must be a Hash containing a $search stage"
|
|
1008
|
+
end
|
|
1009
|
+
body = search_stage["$search"] || search_stage[:"$search"]
|
|
1010
|
+
if body.nil?
|
|
1011
|
+
raise InvalidSearchParameters,
|
|
1012
|
+
"search_stage must contain a $search stage (build it with a SearchBuilder)"
|
|
1013
|
+
end
|
|
1014
|
+
if body.is_a?(Hash)
|
|
1015
|
+
stored = body["returnStoredSource"] || body[:returnStoredSource]
|
|
1016
|
+
if stored
|
|
1017
|
+
raise InvalidSearchParameters,
|
|
1018
|
+
"returnStoredSource is not permitted in a $search stage passed to " \
|
|
1019
|
+
"search_with_stage: Atlas stored-source projection can omit _rperm, " \
|
|
1020
|
+
"which would defeat ACL enforcement on the returned documents."
|
|
1021
|
+
end
|
|
1022
|
+
end
|
|
1023
|
+
search_stage
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def convert_filter_for_mongodb(filter, collection_name, resolution: nil)
|
|
1027
|
+
return filter unless filter
|
|
1028
|
+
|
|
913
1029
|
# The filter hash is interpolated directly into a `$match` stage in
|
|
914
1030
|
# the search pipeline. A caller forwarding a user-controlled filter
|
|
915
1031
|
# (search UI, autocomplete endpoint) must not be able to inject
|
|
916
1032
|
# `$where`, `$function`, `$accumulator`, `$out`, or `$merge` here.
|
|
917
1033
|
# `Parse::PipelineSecurity.validate_filter!` recurses through the
|
|
918
1034
|
# hash and refuses any of those operators at any depth.
|
|
919
|
-
Parse::PipelineSecurity.validate_filter!(filter)
|
|
1035
|
+
Parse::PipelineSecurity.validate_filter!(filter)
|
|
1036
|
+
|
|
1037
|
+
# Additionally refuse `$expr`-based references to protected fields.
|
|
1038
|
+
# `validate_filter!` blocks the code-execution operators above but
|
|
1039
|
+
# NOT `$expr`, which a scoped caller could use to binary-search a
|
|
1040
|
+
# protectedFields value (`{"$expr" => {"$gt" => ["$ssn", "x"]}}`)
|
|
1041
|
+
# even though the field is stripped from the OUTPUT. This is the
|
|
1042
|
+
# same oracle guard the mongo-direct aggregate path applies; the
|
|
1043
|
+
# `filter:` path previously skipped it. `refuse_protected_field_-
|
|
1044
|
+
# references!` self-skips on master / nil resolution / empty
|
|
1045
|
+
# protectedFields, so it's safe to call unconditionally when a
|
|
1046
|
+
# resolution is supplied.
|
|
1047
|
+
if resolution
|
|
1048
|
+
Parse::PipelineSecurity.refuse_protected_field_references!(
|
|
1049
|
+
[{ "$match" => filter }], collection_name, resolution,
|
|
1050
|
+
)
|
|
1051
|
+
end
|
|
1052
|
+
|
|
920
1053
|
filter
|
|
921
1054
|
end
|
|
922
1055
|
|
|
@@ -122,16 +122,16 @@ module Parse
|
|
|
122
122
|
#
|
|
123
123
|
# 1. **Structural pass.** If the body (after whitespace trim) parses as
|
|
124
124
|
# JSON, the parsed structure is walked recursively. Any value whose
|
|
125
|
-
# key matches
|
|
125
|
+
# key matches `SENSITIVE_FIELDS_SET` (case-insensitive) is replaced.
|
|
126
126
|
# String values that themselves look like JSON are recursively
|
|
127
|
-
# parsed and scrubbed — catches
|
|
127
|
+
# parsed and scrubbed — catches `{"body":"{\"password\":\"x\"}"}`
|
|
128
128
|
# payloads.
|
|
129
129
|
#
|
|
130
130
|
# 2. **Regex pass.** The result of the structural pass (or the original
|
|
131
131
|
# string if parsing failed) is always also run through the
|
|
132
|
-
#
|
|
132
|
+
# `SENSITIVE_PATTERN` regex as defense-in-depth. This catches form-
|
|
133
133
|
# encoded bodies, partial JSON, escaped-quote payloads, and string
|
|
134
|
-
# array elements like
|
|
134
|
+
# array elements like `["password=hunter2"]` that the structural
|
|
135
135
|
# walker can't redact in-place.
|
|
136
136
|
# @param str [String] the string to redact.
|
|
137
137
|
# @return [String] the redacted string.
|
|
@@ -153,9 +153,9 @@ module Parse
|
|
|
153
153
|
sep_part = $2
|
|
154
154
|
val_part = $3
|
|
155
155
|
# Skip values that the structural pass already redacted —
|
|
156
|
-
# otherwise the regex value-class
|
|
157
|
-
# bracket and we end up with
|
|
158
|
-
# close-bracket left over from
|
|
156
|
+
# otherwise the regex value-class `[^"&\s,}\]]+` stops at the
|
|
157
|
+
# bracket and we end up with `[FILTERED]]` from the trailing
|
|
158
|
+
# close-bracket left over from `"[FILTERED]"`.
|
|
159
159
|
if val_part == "[FILTERED" || val_part == REDACTED_PLACEHOLDER
|
|
160
160
|
"#{key_part}#{sep_part}#{val_part}"
|
|
161
161
|
else
|
|
@@ -184,7 +184,7 @@ module Parse
|
|
|
184
184
|
#
|
|
185
185
|
# When a value is itself a String that looks like JSON, attempt to
|
|
186
186
|
# parse-scrub-re-encode it so embedded-JSON payloads are also covered
|
|
187
|
-
# (e.g.
|
|
187
|
+
# (e.g. `{"body":"{\"password\":\"x\"}"}`).
|
|
188
188
|
def self.scrub_sensitive!(node)
|
|
189
189
|
case node
|
|
190
190
|
when Hash
|
|
@@ -213,7 +213,7 @@ module Parse
|
|
|
213
213
|
|
|
214
214
|
# @!visibility private
|
|
215
215
|
# Recursively walk a parsed JSON structure replacing any
|
|
216
|
-
# numeric-only Array of length >=
|
|
216
|
+
# numeric-only Array of length >= `LOG_VECTOR_COMPACT_THRESHOLD`
|
|
217
217
|
# with a compact placeholder string ("<vector dims=N>"). Mutates
|
|
218
218
|
# Hashes/Arrays in place; returns the node for chaining. Distinct
|
|
219
219
|
# pass from {scrub_sensitive!} because the criterion is shape
|
|
@@ -259,7 +259,7 @@ module Parse
|
|
|
259
259
|
end
|
|
260
260
|
|
|
261
261
|
# @!visibility private
|
|
262
|
-
# If
|
|
262
|
+
# If `str` parses as JSON (object or array), scrub structurally and
|
|
263
263
|
# re-encode. Otherwise return the original string unchanged.
|
|
264
264
|
def self.maybe_scrub_embedded_json(str)
|
|
265
265
|
return str unless (inner = try_parse_json(str))
|
data/lib/parse/client/logging.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "faraday"
|
|
5
5
|
require "logger"
|
|
6
|
+
require_relative "url_redaction"
|
|
6
7
|
|
|
7
8
|
module Parse
|
|
8
9
|
module Middleware
|
|
@@ -227,8 +228,10 @@ module Parse
|
|
|
227
228
|
end
|
|
228
229
|
|
|
229
230
|
def sanitize_url(url)
|
|
230
|
-
#
|
|
231
|
-
|
|
231
|
+
# Redact credential-bearing query params from logged URLs. Shared
|
|
232
|
+
# with the profiling middleware via Parse::Middleware::URLRedaction
|
|
233
|
+
# so the two sanitizers can't drift.
|
|
234
|
+
Parse::Middleware::URLRedaction.sanitize(url)
|
|
232
235
|
end
|
|
233
236
|
end
|
|
234
237
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "faraday"
|
|
5
|
+
require_relative "url_redaction"
|
|
5
6
|
|
|
6
7
|
module Parse
|
|
7
8
|
module Middleware
|
|
@@ -120,8 +121,10 @@ module Parse
|
|
|
120
121
|
private
|
|
121
122
|
|
|
122
123
|
def sanitize_url(url)
|
|
123
|
-
#
|
|
124
|
-
|
|
124
|
+
# Redact credential-bearing query params. Shared with the logging
|
|
125
|
+
# middleware via Parse::Middleware::URLRedaction so the two
|
|
126
|
+
# sanitizers can't drift (F14 + its profiling twin).
|
|
127
|
+
Parse::Middleware::URLRedaction.sanitize(url)
|
|
125
128
|
end
|
|
126
129
|
|
|
127
130
|
def response_body_size(response_env)
|
|
@@ -33,7 +33,7 @@ module Parse
|
|
|
33
33
|
READ_PREFERENCE = "X-Parse-Read-Preference"
|
|
34
34
|
# The request header field for threading a caller-supplied context object
|
|
35
35
|
# through a write or cloud-function call. Parse Server maps this header to
|
|
36
|
-
#
|
|
36
|
+
# `req.info.context` and flows it through beforeSave/afterSave triggers.
|
|
37
37
|
CLOUD_CONTEXT = "X-Parse-Cloud-Context"
|
|
38
38
|
|
|
39
39
|
# Valid read preference values for MongoDB
|