parse-stack-next 5.8.0 → 5.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +630 -0
- data/README.md +31 -19
- data/docs/caching.md +90 -5
- data/docs/mcp_guide.md +22 -5
- data/docs/mongodb_direct_guide.md +29 -0
- data/docs/webhooks_guide.md +94 -9
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/constraint_translator.rb +22 -21
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/errors.rb +17 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_server.rb +47 -6
- data/lib/parse/agent.rb +521 -70
- data/lib/parse/api/batch.rb +70 -2
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +50 -1
- data/lib/parse/authorization.rb +235 -3
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +263 -11
- data/lib/parse/client/body_builder.rb +100 -15
- data/lib/parse/client/caching.rb +53 -3
- data/lib/parse/client/request.rb +59 -0
- data/lib/parse/client/response.rb +63 -5
- data/lib/parse/client.rb +173 -33
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +142 -11
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/associations/collection_proxy.rb +19 -11
- data/lib/parse/model/associations/has_many.rb +11 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +36 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +97 -12
- data/lib/parse/model/classes/role.rb +23 -0
- data/lib/parse/model/classes/session.rb +307 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +41 -5
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/file.rb +96 -19
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +43 -2
- data/lib/parse/pipeline_security.rb +157 -21
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +665 -41
- data/lib/parse/query.rb +802 -60
- data/lib/parse/stack/tasks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +23 -0
- data/lib/parse/vector_search/hybrid.rb +78 -16
- data/lib/parse/vector_search.rb +215 -7
- data/lib/parse/webhooks/payload.rb +82 -8
- data/lib/parse/webhooks/registration.rb +26 -6
- data/lib/parse/webhooks/replay_protection.rb +19 -9
- data/lib/parse/webhooks.rb +329 -13
- metadata +1 -1
data/lib/parse/client/request.rb
CHANGED
|
@@ -41,6 +41,65 @@ module Parse
|
|
|
41
41
|
# Used to correlate batching requests with their responses.
|
|
42
42
|
attr_accessor :tag
|
|
43
43
|
|
|
44
|
+
# @!visibility private
|
|
45
|
+
# The client this request belongs to, when it was built for a specific
|
|
46
|
+
# one (an object's class client). A {Parse::BatchOperation} sends a
|
|
47
|
+
# request through this client, so a write built for a session-bound
|
|
48
|
+
# client is never batched through another client's credentials. nil
|
|
49
|
+
# means the batch's own client.
|
|
50
|
+
attr_accessor :client
|
|
51
|
+
|
|
52
|
+
# @!visibility private
|
|
53
|
+
# The credentials this request names explicitly, resolved the way
|
|
54
|
+
# {Parse::Client#request} resolves them for a single request: the
|
|
55
|
+
# `session_token:` option, else an `X-Parse-Session-Token` header; and
|
|
56
|
+
# the `use_master_key:` option; and `suppress_master_key: true` when the
|
|
57
|
+
# master-key suppression header is set. A key is absent when the request
|
|
58
|
+
# does not name it, so ambient context (`Parse.with_session`,
|
|
59
|
+
# `client_mode`, a bound client token) still applies.
|
|
60
|
+
#
|
|
61
|
+
# A `session_token:` option that is present but resolves to no token (a
|
|
62
|
+
# user object without one, or a blank string) is returned as `""`, so a
|
|
63
|
+
# batch fails closed the same way a single request does: no master key
|
|
64
|
+
# and no ambient or bound token. `use_master_key:` is returned only when
|
|
65
|
+
# it is exactly `true` or `false`; {Parse::Client#request} treats any
|
|
66
|
+
# other value as not set, and so does a batch.
|
|
67
|
+
# @return [Hash] with optional `:session_token` (String),
|
|
68
|
+
# `:use_master_key` (Boolean), and `:suppress_master_key` (true) keys.
|
|
69
|
+
def explicit_authority
|
|
70
|
+
o = opts.is_a?(Hash) ? opts : {}
|
|
71
|
+
result = {}
|
|
72
|
+
token = Parse::BatchOperation.resolve_session_option(o, :session_token)
|
|
73
|
+
if token.nil?
|
|
74
|
+
header_token = self.class.header_value(headers, Parse::Protocol::SESSION_TOKEN)
|
|
75
|
+
if header_token.is_a?(String)
|
|
76
|
+
token = header_token.strip.empty? ? "" : header_token
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
result[:session_token] = token unless token.nil?
|
|
80
|
+
master = o[:use_master_key]
|
|
81
|
+
result[:use_master_key] = master if master == true || master == false
|
|
82
|
+
# The suppression header wins over `use_master_key: true` in the
|
|
83
|
+
# authentication middleware, so a batch must carry it as a header
|
|
84
|
+
# rather than fold it into `use_master_key:` (which would also change
|
|
85
|
+
# how the ambient and bound session tokens apply).
|
|
86
|
+
if self.class.header_value(headers, Parse::Middleware::Authentication::DISABLE_MASTER_KEY).present?
|
|
87
|
+
result[:suppress_master_key] = true
|
|
88
|
+
end
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @!visibility private
|
|
93
|
+
# Case-insensitive header lookup.
|
|
94
|
+
# @return [Object, nil]
|
|
95
|
+
def self.header_value(headers, name)
|
|
96
|
+
return nil unless headers.is_a?(Hash)
|
|
97
|
+
return headers[name] if headers.key?(name)
|
|
98
|
+
wanted = name.to_s.downcase
|
|
99
|
+
headers.each { |k, v| return v if k.to_s.downcase == wanted }
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
44
103
|
# @!attribute [rw] request_id
|
|
45
104
|
# @return [String] unique identifier for this request to enable idempotency
|
|
46
105
|
attr_accessor :request_id
|
|
@@ -264,19 +264,77 @@ module Parse
|
|
|
264
264
|
self
|
|
265
265
|
end
|
|
266
266
|
|
|
267
|
+
# A summary that never prints result values: login, signup, and
|
|
268
|
+
# `users/me` responses carry a live `sessionToken` (and sometimes
|
|
269
|
+
# `authData`), and any other row can carry private data, so inspect
|
|
270
|
+
# shows only the shape of the result.
|
|
267
271
|
# @!visibility private
|
|
268
272
|
def inspect
|
|
269
273
|
if error?
|
|
270
|
-
"#<#{self.class} @code=#{code} @error='#{
|
|
274
|
+
"#<#{self.class} @code=#{code} @error='#{safe_error_text}' @http_status=#{http_status.inspect}>"
|
|
271
275
|
else
|
|
272
|
-
"#<#{self.class} @
|
|
276
|
+
"#<#{self.class} @http_status=#{http_status.inspect} @result=#{result_summary}>"
|
|
273
277
|
end
|
|
274
278
|
end
|
|
275
279
|
|
|
276
|
-
# @return [String] JSON encoded object, or an error string.
|
|
280
|
+
# @return [String] JSON encoded object, or an error string. Credential
|
|
281
|
+
# fields (`sessionToken`, `password`, `authData`, MFA recovery codes
|
|
282
|
+
# and secrets, keys) are replaced with a placeholder, and credential
|
|
283
|
+
# text inside string values (`password=x`, `X-Parse-Master-Key: mk`)
|
|
284
|
+
# is filtered too, so printing the response with `to_s` or `inspect`
|
|
285
|
+
# does not leak a live session token. The error form has its text redacted and control characters
|
|
286
|
+
# escaped. `#result`, `#to_json`, and `#as_json` return the raw values
|
|
287
|
+
# by design; do not log those.
|
|
277
288
|
def to_s
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
if error?
|
|
290
|
+
return "[E-#{@code}] #{safe_request_text} : #{safe_error_text} (#{@http_status})"
|
|
291
|
+
end
|
|
292
|
+
result = redacted_result
|
|
293
|
+
return Parse::Middleware::BodyBuilder.redact_patterns(result) if result.is_a?(String)
|
|
294
|
+
# Text patterns run on string values before encoding, never on the
|
|
295
|
+
# encoded JSON, so escaped quotes cannot defeat them or corrupt it.
|
|
296
|
+
Parse::Middleware::BodyBuilder.redact_string_values!(result).to_json
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
private
|
|
300
|
+
|
|
301
|
+
# Maximum error text length kept in `to_s` / `inspect`.
|
|
302
|
+
SAFE_ERROR_TEXT_LENGTH = 1_000
|
|
303
|
+
|
|
304
|
+
# The server's error text with credentials redacted, truncated, and
|
|
305
|
+
# control characters escaped, matching what the client logs.
|
|
306
|
+
def safe_error_text
|
|
307
|
+
text = Parse::Middleware::BodyBuilder.redact(@error.to_s)[0, SAFE_ERROR_TEXT_LENGTH]
|
|
308
|
+
Parse::TerminalSafe.sanitize_line(text)
|
|
309
|
+
rescue StandardError
|
|
310
|
+
"[unprintable error]"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The request line (method and path, including any query string) with
|
|
314
|
+
# credentials redacted and control characters escaped.
|
|
315
|
+
def safe_request_text
|
|
316
|
+
Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(@request.to_s))
|
|
317
|
+
rescue StandardError
|
|
318
|
+
"[unprintable request]"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Class and size of the result, without its values.
|
|
322
|
+
def result_summary
|
|
323
|
+
case @result
|
|
324
|
+
when Array then "Array(#{@result.size})"
|
|
325
|
+
when Hash then "Hash(#{@result.size} keys)"
|
|
326
|
+
when nil then "nil"
|
|
327
|
+
else @result.class.name
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# A deep copy of the result with credential fields replaced.
|
|
332
|
+
def redacted_result
|
|
333
|
+
return @result unless @result.is_a?(Hash) || @result.is_a?(Array)
|
|
334
|
+
copy = JSON.parse(@result.to_json)
|
|
335
|
+
Parse::Middleware::BodyBuilder.scrub_sensitive!(copy)
|
|
336
|
+
rescue StandardError
|
|
337
|
+
"[unprintable result]"
|
|
280
338
|
end
|
|
281
339
|
end
|
|
282
340
|
end
|
data/lib/parse/client.rb
CHANGED
|
@@ -283,6 +283,14 @@ module Parse
|
|
|
283
283
|
DEFAULT_RETRIES = 2
|
|
284
284
|
# The wait time in seconds between retries
|
|
285
285
|
RETRY_DELAY = 1.5
|
|
286
|
+
# SDK-internal value for the `metadata_master:` request option. A request
|
|
287
|
+
# that passes it together with `use_master_key: true` reads metadata (a
|
|
288
|
+
# class schema, the role graph) and keeps the master key inside
|
|
289
|
+
# {Parse.without_master_key}. Only this exact object turns it on.
|
|
290
|
+
# @!visibility private
|
|
291
|
+
METADATA_MASTER_REQUEST = Object.new.tap do |o|
|
|
292
|
+
def o.inspect = "#<Parse::Client::METADATA_MASTER_REQUEST>"
|
|
293
|
+
end.freeze
|
|
286
294
|
|
|
287
295
|
# An error when a general response error occurs when communicating with Parse server.
|
|
288
296
|
class ResponseError < Parse::Error; end
|
|
@@ -411,6 +419,39 @@ module Parse
|
|
|
411
419
|
"session_token=#{@session_token ? "[FILTERED]" : "nil"}>"
|
|
412
420
|
end
|
|
413
421
|
|
|
422
|
+
# Redacted JSON form. ActiveSupport's default `as_json` serializes every
|
|
423
|
+
# instance variable, so a client in a JSON log or error-tracker context
|
|
424
|
+
# (or inside an object that serializes it, such as a {Parse::Agent})
|
|
425
|
+
# would emit the master key, REST key, and bound session token.
|
|
426
|
+
# @return [Hash]
|
|
427
|
+
def as_json(*)
|
|
428
|
+
{
|
|
429
|
+
"server_url" => @server_url,
|
|
430
|
+
"app_id" => @application_id,
|
|
431
|
+
"master_key" => @master_key ? "[FILTERED]" : nil,
|
|
432
|
+
"session_token" => @session_token ? "[FILTERED]" : nil,
|
|
433
|
+
}
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# @return [String] the redacted {#as_json} summary as JSON.
|
|
437
|
+
def to_json(*args)
|
|
438
|
+
as_json.to_json(*args)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# YAML (Psych) serializes instance variables too; emit the redacted
|
|
442
|
+
# summary instead.
|
|
443
|
+
def encode_with(coder)
|
|
444
|
+
as_json.each { |k, v| coder[k] = v }
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# A client holds the master key, REST key, and session tokens, so it is
|
|
448
|
+
# never marshaled (Rails.cache, DRb, or a job payload would store them in
|
|
449
|
+
# the clear). Store the configuration and build a new client instead.
|
|
450
|
+
# @raise [TypeError]
|
|
451
|
+
def marshal_dump
|
|
452
|
+
raise TypeError, "Parse::Client cannot be marshaled"
|
|
453
|
+
end
|
|
454
|
+
|
|
414
455
|
# A NEW non-master {Parse::Client} that mirrors THIS client's connection
|
|
415
456
|
# settings (`server_url` / `application_id` / `api_key`) but carries no
|
|
416
457
|
# master key and binds `session_token`, so it acts on the server as that
|
|
@@ -637,6 +678,12 @@ module Parse
|
|
|
637
678
|
# middleware. The default value is 3 seconds. If :expires is set to 0,
|
|
638
679
|
# caching will be disabled. You can always clear the current state of the
|
|
639
680
|
# cache using the clear_cache! method on your Parse::Client instance.
|
|
681
|
+
# @option opts [Boolean] :cache_session_requests Whether this client's
|
|
682
|
+
# response cache stores and serves reads made with a session token.
|
|
683
|
+
# Overrides {Parse::Middleware::Caching.cache_session_requests} in
|
|
684
|
+
# either direction; omit it to use that class default (off). Only
|
|
685
|
+
# `true` enables it. With it on, a revoked session keeps reading its
|
|
686
|
+
# cached rows until the entry expires.
|
|
640
687
|
# @option opts [String] :cache_namespace Optional prefix applied to every
|
|
641
688
|
# cache key. Useful when two Parse apps share one Redis instance and
|
|
642
689
|
# would otherwise collide on identical paths (e.g.
|
|
@@ -696,7 +743,9 @@ module Parse
|
|
|
696
743
|
@allow_faraday_proxy = opts.fetch(:allow_faraday_proxy, false)
|
|
697
744
|
|
|
698
745
|
# Security check for HTTP usage (except localhost/127.0.0.1 for development)
|
|
699
|
-
|
|
746
|
+
# The scheme and host come from URI parsing, so `HTTP://` and leading
|
|
747
|
+
# whitespace are treated as plain http, not as a secure URL.
|
|
748
|
+
if self.class.url_scheme(@server_url) == "http" && !self.class.loopback_host?(self.class.url_host(@server_url))
|
|
700
749
|
if @require_https
|
|
701
750
|
raise ArgumentError, "[Parse::Client] HTTPS required but server URL uses HTTP: #{@server_url}. " \
|
|
702
751
|
"Set require_https: false or use an HTTPS URL."
|
|
@@ -757,7 +806,7 @@ module Parse
|
|
|
757
806
|
# scheme; without this guard a caller passing
|
|
758
807
|
# faraday: { ssl: { verify: false }, proxy: "http://attacker" }
|
|
759
808
|
# would neuter TLS verification on an HTTPS connection.
|
|
760
|
-
validate_faraday_opts!(opts[:faraday])
|
|
809
|
+
opts[:faraday] = validate_faraday_opts!(opts[:faraday])
|
|
761
810
|
opts[:faraday].merge!(:url => @server_url)
|
|
762
811
|
@conn = Faraday.new(opts[:faraday]) do |conn|
|
|
763
812
|
# Apply timeouts before any user-supplied middleware sees a request.
|
|
@@ -929,6 +978,10 @@ module Parse
|
|
|
929
978
|
# old workers still read the legacy shape, so invalidation has to
|
|
930
979
|
# hit both until every old worker is drained.
|
|
931
980
|
delete_legacy_variants: opts.fetch(:cache_delete_legacy_variants, true),
|
|
981
|
+
# Per-client override of
|
|
982
|
+
# Parse::Middleware::Caching.cache_session_requests; omitted
|
|
983
|
+
# means the class default applies.
|
|
984
|
+
**(opts.key?(:cache_session_requests) ? { cache_session_requests: opts[:cache_session_requests] } : {}),
|
|
932
985
|
}
|
|
933
986
|
|
|
934
987
|
# Inform about opt-in cache behavior
|
|
@@ -978,19 +1031,27 @@ module Parse
|
|
|
978
1031
|
# controlled MITM unless explicitly allowlisted
|
|
979
1032
|
#
|
|
980
1033
|
# @api private
|
|
1034
|
+
# @return [Hash] the options as a Hash, so a `Faraday::ConnectionOptions`
|
|
1035
|
+
# (or anything else responding to `to_hash`) is checked and used in the
|
|
1036
|
+
# same form; any other value raises.
|
|
981
1037
|
def validate_faraday_opts!(faraday_opts)
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if ssl.is_a?(Hash)
|
|
986
|
-
verify = ssl.key?(:verify) ? ssl[:verify] : ssl["verify"]
|
|
987
|
-
if verify == false && @server_url.to_s.start_with?("https://")
|
|
1038
|
+
faraday_opts = {} if faraday_opts.nil?
|
|
1039
|
+
unless faraday_opts.is_a?(Hash)
|
|
1040
|
+
unless faraday_opts.respond_to?(:to_hash)
|
|
988
1041
|
raise ArgumentError,
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
"Fix the server certificate or downgrade the URL to http:// " \
|
|
992
|
-
"(with require_https: false) for explicit local testing."
|
|
1042
|
+
"[Parse::Client] opts[:faraday] must be a Hash or Faraday::ConnectionOptions " \
|
|
1043
|
+
"(got #{faraday_opts.class})."
|
|
993
1044
|
end
|
|
1045
|
+
faraday_opts = faraday_opts.to_hash
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
ssl = faraday_opts[:ssl] || faraday_opts["ssl"]
|
|
1049
|
+
if self.class.url_scheme(@server_url) == "https" && (setting = tls_verification_disabled(ssl))
|
|
1050
|
+
raise ArgumentError,
|
|
1051
|
+
"[Parse::Client] Refusing to disable TLS certificate verification " \
|
|
1052
|
+
"(opts[:faraday][:ssl] #{setting}) on an HTTPS server URL. " \
|
|
1053
|
+
"Fix the server certificate or downgrade the URL to http:// " \
|
|
1054
|
+
"(with require_https: false) for explicit local testing."
|
|
994
1055
|
end
|
|
995
1056
|
|
|
996
1057
|
proxy = faraday_opts[:proxy] || faraday_opts["proxy"]
|
|
@@ -1010,15 +1071,82 @@ module Parse
|
|
|
1010
1071
|
# `proxy: nil` is the Faraday-documented way to disable
|
|
1011
1072
|
# env-proxy autodiscovery.
|
|
1012
1073
|
faraday_opts[:proxy] = nil unless @allow_faraday_proxy
|
|
1074
|
+
faraday_opts
|
|
1013
1075
|
end
|
|
1014
1076
|
|
|
1015
1077
|
private :validate_faraday_opts!
|
|
1016
1078
|
|
|
1017
|
-
#
|
|
1018
|
-
#
|
|
1019
|
-
#
|
|
1020
|
-
#
|
|
1021
|
-
|
|
1079
|
+
# Which TLS setting in a Faraday `ssl:` option (a Hash or a
|
|
1080
|
+
# `Faraday::SSLOptions`) turns certificate or hostname verification off,
|
|
1081
|
+
# or nil when none does: `verify: false`, `verify_mode: VERIFY_NONE`, or
|
|
1082
|
+
# `verify_hostname: false`.
|
|
1083
|
+
# @api private
|
|
1084
|
+
def tls_verification_disabled(ssl)
|
|
1085
|
+
read = lambda do |key|
|
|
1086
|
+
if ssl.is_a?(Hash)
|
|
1087
|
+
ssl.key?(key) ? ssl[key] : ssl[key.to_s]
|
|
1088
|
+
elsif ssl.respond_to?(key)
|
|
1089
|
+
ssl.public_send(key)
|
|
1090
|
+
end
|
|
1091
|
+
end
|
|
1092
|
+
return nil if ssl.nil?
|
|
1093
|
+
return "verify: false" if read.call(:verify) == false
|
|
1094
|
+
if defined?(OpenSSL::SSL::VERIFY_NONE) && read.call(:verify_mode) == OpenSSL::SSL::VERIFY_NONE
|
|
1095
|
+
return "verify_mode: VERIFY_NONE"
|
|
1096
|
+
end
|
|
1097
|
+
return "verify_hostname: false" if read.call(:verify_hostname) == false
|
|
1098
|
+
nil
|
|
1099
|
+
end
|
|
1100
|
+
private :tls_verification_disabled
|
|
1101
|
+
|
|
1102
|
+
# Whether a URL host is this machine: `localhost`, any `127.0.0.0/8`
|
|
1103
|
+
# address, `::1`, or `0.0.0.0` (a connect to the unspecified address
|
|
1104
|
+
# reaches the local host on Linux and macOS). Used by the http:// server
|
|
1105
|
+
# warning and the LiveQuery cleartext guards so they agree.
|
|
1106
|
+
# @param host [String, nil]
|
|
1107
|
+
# @return [Boolean]
|
|
1108
|
+
# @api private
|
|
1109
|
+
#
|
|
1110
|
+
# Addresses are parsed with IPAddr, so a malformed one such as
|
|
1111
|
+
# `127.999.1.1` (which a resolver would look up by name) is not loopback.
|
|
1112
|
+
# `localhost.` with a trailing dot is not either: some resolvers skip
|
|
1113
|
+
# `/etc/hosts` for the fully qualified form. `0.0.0.0` stays local for
|
|
1114
|
+
# client URLs only (it is a bind address everywhere else).
|
|
1115
|
+
def self.loopback_host?(host)
|
|
1116
|
+
h = host.to_s.strip.downcase.delete_prefix("[").delete_suffix("]")
|
|
1117
|
+
return false if h.empty?
|
|
1118
|
+
return true if h == "localhost" || h == "0.0.0.0"
|
|
1119
|
+
return false unless h.match?(/\A[0-9a-f:.]+\z/)
|
|
1120
|
+
require "ipaddr"
|
|
1121
|
+
IPAddr.new(h).loopback?
|
|
1122
|
+
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
|
|
1123
|
+
false
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
# The lowercased scheme of a URL String, or nil when it does not parse.
|
|
1127
|
+
# Leading and trailing whitespace is ignored. Scheme checks go through
|
|
1128
|
+
# this instead of a case-sensitive prefix match, which let `HTTP://` and
|
|
1129
|
+
# `WS://` past the plaintext guards.
|
|
1130
|
+
# @param url [String, nil]
|
|
1131
|
+
# @return [String, nil]
|
|
1132
|
+
# @api private
|
|
1133
|
+
def self.url_scheme(url)
|
|
1134
|
+
return nil if url.nil?
|
|
1135
|
+
URI.parse(url.to_s.strip).scheme&.downcase
|
|
1136
|
+
rescue URI::InvalidURIError
|
|
1137
|
+
nil
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
# The lowercased host of a URL String, or nil when it does not parse.
|
|
1141
|
+
# @param url [String, nil]
|
|
1142
|
+
# @return [String, nil]
|
|
1143
|
+
# @api private
|
|
1144
|
+
def self.url_host(url)
|
|
1145
|
+
return nil if url.nil?
|
|
1146
|
+
URI.parse(url.to_s.strip).host&.downcase
|
|
1147
|
+
rescue URI::InvalidURIError
|
|
1148
|
+
nil
|
|
1149
|
+
end
|
|
1022
1150
|
|
|
1023
1151
|
# Configure LiveQuery with the given options
|
|
1024
1152
|
# @param opts [Hash] configuration options
|
|
@@ -1041,7 +1169,7 @@ module Parse
|
|
|
1041
1169
|
# `live_query: { url: "ws://prod-host" }` or
|
|
1042
1170
|
# `live_query_url: "ws://prod-host"` bypassed it — the master key
|
|
1043
1171
|
# and any session token would ride the connect frame in cleartext.
|
|
1044
|
-
validate_live_query_url!(resolved_url, allow_insecure: live_query_opts[:allow_insecure])
|
|
1172
|
+
resolved_url = validate_live_query_url!(resolved_url, allow_insecure: live_query_opts[:allow_insecure])
|
|
1045
1173
|
|
|
1046
1174
|
# Warn (don't raise) on `live_query: { ... }` keys that are not
|
|
1047
1175
|
# `Parse::LiveQuery::Configuration` setters. The block form would
|
|
@@ -1071,20 +1199,14 @@ module Parse
|
|
|
1071
1199
|
end
|
|
1072
1200
|
end
|
|
1073
1201
|
|
|
1202
|
+
# Validate an explicit LiveQuery URL at configure time with the same
|
|
1203
|
+
# rules {Parse::LiveQuery::Client} applies, and return the URL to use
|
|
1204
|
+
# (`http://` and `https://` are mapped to `ws://` and `wss://`).
|
|
1205
|
+
# @return [String, nil]
|
|
1074
1206
|
# @api private
|
|
1075
1207
|
def validate_live_query_url!(url, allow_insecure:)
|
|
1076
|
-
return unless url.is_a?(String)
|
|
1077
|
-
|
|
1078
|
-
host = URI.parse(url).host.to_s rescue ""
|
|
1079
|
-
return if LIVE_QUERY_LOOPBACK_HOSTS.include?(host)
|
|
1080
|
-
return if allow_insecure
|
|
1081
|
-
|
|
1082
|
-
raise ArgumentError,
|
|
1083
|
-
"[Parse::Client] Refusing explicit insecure LiveQuery URL #{url.inspect}. " \
|
|
1084
|
-
"The connect frame carries the master key and any session token in " \
|
|
1085
|
-
"plaintext on this socket. Use wss:// for routable hosts, or pass " \
|
|
1086
|
-
"`live_query: { allow_insecure: true }` to opt into cleartext for " \
|
|
1087
|
-
"local development on a non-loopback address."
|
|
1208
|
+
return url unless url.is_a?(String)
|
|
1209
|
+
Parse::LiveQuery::Client.normalize_url(url, allow_insecure: allow_insecure)
|
|
1088
1210
|
end
|
|
1089
1211
|
|
|
1090
1212
|
# @api private
|
|
@@ -1319,7 +1441,10 @@ module Parse
|
|
|
1319
1441
|
body ||= _request.body
|
|
1320
1442
|
headers.merge! _request.headers
|
|
1321
1443
|
else
|
|
1322
|
-
|
|
1444
|
+
# The metadata-master sentinel stays off the stored request: a
|
|
1445
|
+
# caller replaying `response.request` later must not inherit it.
|
|
1446
|
+
_request = Parse::Request.new(method, uri, body: body, headers: headers,
|
|
1447
|
+
opts: opts.except(:metadata_master))
|
|
1323
1448
|
# Request copies the headers it is given, so carry its request id
|
|
1324
1449
|
# back onto the outgoing headers.
|
|
1325
1450
|
headers.merge!(_request.headers)
|
|
@@ -1348,6 +1473,14 @@ module Parse
|
|
|
1348
1473
|
# never sent unless the caller explicitly passed `use_master_key: true`
|
|
1349
1474
|
explicit_master = opts.key?(:use_master_key)
|
|
1350
1475
|
|
|
1476
|
+
# SDK metadata reads (class schemas, the role graph) keep the master
|
|
1477
|
+
# key inside `Parse.without_master_key`. Only the internal sentinel
|
|
1478
|
+
# turns this on; a `true` or any other value from a caller does not.
|
|
1479
|
+
# The marker goes on a per-attempt copy of the headers handed to the
|
|
1480
|
+
# connection (below), never on `headers` or the stored request, so a
|
|
1481
|
+
# retry, `response.request`, or an error reporter never carries it.
|
|
1482
|
+
metadata_master = opts[:metadata_master].equal?(METADATA_MASTER_REQUEST) && opts[:use_master_key] == true
|
|
1483
|
+
|
|
1351
1484
|
if opts[:use_master_key] == false
|
|
1352
1485
|
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
1353
1486
|
elsif Parse.client_mode && opts[:use_master_key] != true
|
|
@@ -1364,7 +1497,9 @@ module Parse
|
|
|
1364
1497
|
# `with_session(token_b)` resolved user B, and the identity cache
|
|
1365
1498
|
# then mapped token A (or a garbage token) to user B for its TTL.
|
|
1366
1499
|
if raw_token.nil?
|
|
1367
|
-
|
|
1500
|
+
# Same lookup a batch uses for each request's own credentials
|
|
1501
|
+
# (Parse::Request#explicit_authority).
|
|
1502
|
+
header_token = Parse::Request.header_value(headers, Parse::Protocol::SESSION_TOKEN)
|
|
1368
1503
|
raw_token = header_token if header_token.is_a?(String)
|
|
1369
1504
|
end
|
|
1370
1505
|
# SEC-02:an EXPLICITLY-supplied session_token that is a blank /
|
|
@@ -1417,7 +1552,12 @@ module Parse
|
|
|
1417
1552
|
params = (method == :get ? query : body) || {}
|
|
1418
1553
|
# if the path does not start with the '/1/' prefix, then add it to be nice.
|
|
1419
1554
|
# actually send the request and return the body
|
|
1420
|
-
|
|
1555
|
+
send_headers = headers
|
|
1556
|
+
if metadata_master
|
|
1557
|
+
send_headers = headers.merge(Parse::Middleware::Authentication::METADATA_MASTER =>
|
|
1558
|
+
Parse::Middleware::Authentication::METADATA_MASTER_TOKEN)
|
|
1559
|
+
end
|
|
1560
|
+
response_env = @conn.send(method, uri, params, send_headers)
|
|
1421
1561
|
response = response_env.body
|
|
1422
1562
|
response.request = _request
|
|
1423
1563
|
|
data/lib/parse/clp_scope.rb
CHANGED
|
@@ -86,7 +86,14 @@ module Parse
|
|
|
86
86
|
# at request rate.
|
|
87
87
|
NEGATIVE_TTL = 5
|
|
88
88
|
|
|
89
|
+
# How long a schema fetch that failed inside `Parse.without_master_key`
|
|
90
|
+
# is remembered. Kept apart from the shared cache and consulted only
|
|
91
|
+
# inside the block, so it never denies another caller's reads; it only
|
|
92
|
+
# stops a down server from being refetched on every read in the block.
|
|
93
|
+
SUPPRESSED_NEGATIVE_TTL = 2
|
|
94
|
+
|
|
89
95
|
@cache = {}
|
|
96
|
+
@suppressed_failures = {}
|
|
90
97
|
@cache_mutex = Mutex.new
|
|
91
98
|
@cache_ttl = POSITIVE_TTL
|
|
92
99
|
|
|
@@ -490,7 +497,10 @@ module Parse
|
|
|
490
497
|
end
|
|
491
498
|
|
|
492
499
|
def reset_cache!
|
|
493
|
-
@cache_mutex.synchronize
|
|
500
|
+
@cache_mutex.synchronize do
|
|
501
|
+
@cache.clear
|
|
502
|
+
@suppressed_failures.clear
|
|
503
|
+
end
|
|
494
504
|
# Also drop the unresolvable-class warned-once registry so
|
|
495
505
|
# tests that assert on `warn` emission for a class don't get
|
|
496
506
|
# silenced by an earlier test's call.
|
|
@@ -599,6 +609,12 @@ module Parse
|
|
|
599
609
|
cached = @cache_mutex.synchronize { @cache[key] }
|
|
600
610
|
return cached if cached && !stale?(cached)
|
|
601
611
|
|
|
612
|
+
suppressed = Parse.respond_to?(:master_key_disabled?) && Parse.master_key_disabled?
|
|
613
|
+
if suppressed
|
|
614
|
+
failed_at = @cache_mutex.synchronize { @suppressed_failures[key] }
|
|
615
|
+
return unresolvable_entry if failed_at && monotonic_now - failed_at < SUPPRESSED_NEGATIVE_TTL
|
|
616
|
+
end
|
|
617
|
+
|
|
602
618
|
entry = if resolved_client.nil?
|
|
603
619
|
# No client configured (Parse.setup never called, etc.) —
|
|
604
620
|
# treat as unresolvable so we fail closed instead of
|
|
@@ -620,7 +636,17 @@ module Parse
|
|
|
620
636
|
end
|
|
621
637
|
end
|
|
622
638
|
|
|
623
|
-
|
|
639
|
+
# A failure inside `Parse.without_master_key` is not put in the
|
|
640
|
+
# shared cache. The request keeps the master key there, but a custom
|
|
641
|
+
# schema source may not, and a negative entry is shared by every
|
|
642
|
+
# caller of this class for {NEGATIVE_TTL}: one block must not deny
|
|
643
|
+
# every other request's scoped reads. It is remembered briefly in a
|
|
644
|
+
# separate table read only inside the block.
|
|
645
|
+
if entry.kind == :unresolvable && suppressed
|
|
646
|
+
@cache_mutex.synchronize { @suppressed_failures[key] = monotonic_now }
|
|
647
|
+
else
|
|
648
|
+
@cache_mutex.synchronize { @cache[key] = entry }
|
|
649
|
+
end
|
|
624
650
|
entry
|
|
625
651
|
end
|
|
626
652
|
|
|
@@ -637,6 +663,13 @@ module Parse
|
|
|
637
663
|
# and the client-bound session token. A client that holds no master key
|
|
638
664
|
# still cannot read schemas, which stays fail-closed.
|
|
639
665
|
#
|
|
666
|
+
# The schema is metadata, so the request also carries the SDK's
|
|
667
|
+
# metadata marker: it keeps the master key inside
|
|
668
|
+
# `Parse.without_master_key`, which strips it from every other request
|
|
669
|
+
# in the block. Without it the fetch was refused there and the
|
|
670
|
+
# `:unresolvable` entry denied every scoped read of the class, for all
|
|
671
|
+
# callers, until it expired.
|
|
672
|
+
#
|
|
640
673
|
# Objects that only implement `#schema` (test doubles, custom schema
|
|
641
674
|
# sources installed via {.schema_client}), and clients whose `#schema`
|
|
642
675
|
# was overridden on the instance, keep the old call.
|
|
@@ -649,7 +682,8 @@ module Parse
|
|
|
649
682
|
if resolved_client.is_a?(Parse::Client) && stock_schema
|
|
650
683
|
safe = Parse::API::PathSegment.identifier!(class_key, kind: "class name")
|
|
651
684
|
resolved_client.request(:get, "schemas/#{safe}",
|
|
652
|
-
opts: { cache: false, use_master_key: true
|
|
685
|
+
opts: { cache: false, use_master_key: true,
|
|
686
|
+
metadata_master: Parse::Client::METADATA_MASTER_REQUEST })
|
|
653
687
|
else
|
|
654
688
|
resolved_client.schema(class_key)
|
|
655
689
|
end
|