parse-stack-next 5.7.6 → 5.8.1
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 +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
data/lib/parse/agent.rb
CHANGED
|
@@ -23,6 +23,8 @@ require_relative "agent/rate_limiter"
|
|
|
23
23
|
require_relative "agent/cancellation_token"
|
|
24
24
|
require_relative "agent/log_levels"
|
|
25
25
|
require_relative "agent/approval_gate"
|
|
26
|
+
require_relative "agent/field_policy"
|
|
27
|
+
require_relative "agent/field_names"
|
|
26
28
|
require_relative "agent/prompt_hardening"
|
|
27
29
|
require_relative "agent/describe"
|
|
28
30
|
|
|
@@ -592,8 +594,10 @@ module Parse
|
|
|
592
594
|
# Use provided port, or configured port, or default
|
|
593
595
|
port ||= Parse.mcp_server_port || 3001
|
|
594
596
|
|
|
595
|
-
|
|
597
|
+
# Load the server before flagging MCP as enabled, so a failed load
|
|
598
|
+
# does not leave mcp_enabled? reporting true.
|
|
596
599
|
require_relative "agent/mcp_server"
|
|
600
|
+
@mcp_enabled = true
|
|
597
601
|
MCPServer.default_port = port
|
|
598
602
|
|
|
599
603
|
# Pass remote API config if available
|
|
@@ -1031,11 +1035,35 @@ module Parse
|
|
|
1031
1035
|
# `progress_callback` / `cancellation_token` are threaded. An
|
|
1032
1036
|
# embedder on the non-MCP path may assign any object responding to
|
|
1033
1037
|
# `#review`.
|
|
1038
|
+
#
|
|
1039
|
+
# A sub-agent built with `parent:` and no gate of its own uses its
|
|
1040
|
+
# parent's gate, read at call time: the dispatcher swaps the
|
|
1041
|
+
# parent's gate per request, so a copy taken at construction would
|
|
1042
|
+
# go stale. Without this a sub-agent fell back to {NullGate} and
|
|
1043
|
+
# ran tools in `require_approval_for` tiers with no approval.
|
|
1034
1044
|
def approval_gate
|
|
1035
|
-
@approval_gate
|
|
1045
|
+
return @approval_gate if @approval_gate
|
|
1046
|
+
return @approval_parent.approval_gate if @approval_parent
|
|
1047
|
+
@approval_gate_defaulted = true
|
|
1048
|
+
@approval_gate = Parse::Agent::NullGate.new
|
|
1036
1049
|
end
|
|
1037
1050
|
|
|
1038
|
-
|
|
1051
|
+
# Assign this agent's own approval gate. Assigning nil removes it, so
|
|
1052
|
+
# a sub-agent goes back to using its parent's gate.
|
|
1053
|
+
def approval_gate=(gate)
|
|
1054
|
+
@approval_gate_defaulted = false
|
|
1055
|
+
@approval_gate = gate
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
# @return [Parse::Agent::ApprovalGate, nil] the gate assigned on this
|
|
1059
|
+
# agent itself, or nil when it uses its parent's (or the default).
|
|
1060
|
+
# The MCP dispatcher snapshots and restores this rather than
|
|
1061
|
+
# {#approval_gate}, so restoring never pins a parent's gate.
|
|
1062
|
+
# @api private
|
|
1063
|
+
def approval_gate_override
|
|
1064
|
+
gate = @approval_gate
|
|
1065
|
+
gate.is_a?(Parse::Agent::NullGate) && @approval_gate_defaulted ? nil : gate
|
|
1066
|
+
end
|
|
1039
1067
|
|
|
1040
1068
|
# @return [Boolean] true if the active cancellation token has been
|
|
1041
1069
|
# tripped; false otherwise. Returns false when no token is
|
|
@@ -1559,6 +1587,7 @@ module Parse
|
|
|
1559
1587
|
max_log_size: DEFAULT_MAX_LOG_SIZE,
|
|
1560
1588
|
system_prompt: nil, system_prompt_suffix: nil, pricing: nil,
|
|
1561
1589
|
tools: nil, methods: nil, classes: nil, filters: nil,
|
|
1590
|
+
fields: nil, field_names: nil,
|
|
1562
1591
|
parent: nil, recursion_depth: nil,
|
|
1563
1592
|
strict_tool_filter: nil, strict_class_filter: nil,
|
|
1564
1593
|
master_atlas: nil,
|
|
@@ -1731,8 +1760,10 @@ module Parse
|
|
|
1731
1760
|
#
|
|
1732
1761
|
# * nil — inherit from parent (the common case; the
|
|
1733
1762
|
# child wants whatever the parent had).
|
|
1734
|
-
# * true
|
|
1735
|
-
#
|
|
1763
|
+
# * true: keep faceted_search authority. Allowed only when
|
|
1764
|
+
# the parent has it: a child cannot turn on
|
|
1765
|
+
# master-key faceting under a scoped parent, which
|
|
1766
|
+
# would return every row's buckets and results.
|
|
1736
1767
|
# * false — explicit opt-OUT: the sub-agent should DROP
|
|
1737
1768
|
# faceted_search authority even if the parent
|
|
1738
1769
|
# had it. Previously `false` was the default
|
|
@@ -1748,6 +1779,13 @@ module Parse
|
|
|
1748
1779
|
# per-row ACL via Parse::ACLScope's `_rperm` match and do
|
|
1749
1780
|
# NOT consult master_atlas.
|
|
1750
1781
|
master_atlas = parent.master_atlas if master_atlas.nil?
|
|
1782
|
+
if master_atlas == true && !parent.master_atlas
|
|
1783
|
+
raise ArgumentError,
|
|
1784
|
+
"sub-agent master_atlas: true exceeds parent's master_atlas: false. " \
|
|
1785
|
+
"A sub-agent cannot turn on master-key Atlas faceting its parent " \
|
|
1786
|
+
"was not given. Omit master_atlas to inherit the parent's setting, " \
|
|
1787
|
+
"or pass master_atlas: false."
|
|
1788
|
+
end
|
|
1751
1789
|
|
|
1752
1790
|
# Inherit cooperative cancellation surface. Without this, a
|
|
1753
1791
|
# delegating tool that constructs a sub-agent and drives it
|
|
@@ -1759,6 +1797,9 @@ module Parse
|
|
|
1759
1797
|
@cancellation_token = parent.cancellation_token
|
|
1760
1798
|
@progress_callback = parent.progress_callback
|
|
1761
1799
|
@log_callback = parent.log_callback
|
|
1800
|
+
# Approval gate: delegate to the parent's current gate (see
|
|
1801
|
+
# {#approval_gate}).
|
|
1802
|
+
@approval_parent = parent
|
|
1762
1803
|
|
|
1763
1804
|
# Clamp the sub-agent's permission tier at the parent's. The
|
|
1764
1805
|
# default :readonly is always ≤ any parent tier, so this fires
|
|
@@ -2033,9 +2074,79 @@ module Parse
|
|
|
2033
2074
|
@method_filter_only, @method_filter_except = normalize_method_filter(methods)
|
|
2034
2075
|
@class_filter_only, @class_filter_except = normalize_class_filter(classes)
|
|
2035
2076
|
@filters = normalize_query_filters(filters)
|
|
2077
|
+
# Per-agent field narrowing (see Parse::Agent::FieldPolicy). Stored as
|
|
2078
|
+
# layers so a sub-agent's policy INTERSECTS its parent's: every layer
|
|
2079
|
+
# that names a class must permit a field for the agent to see it.
|
|
2080
|
+
own_field_policy = normalize_field_policy(fields)
|
|
2081
|
+
@field_policy_layers = (parent ? parent.field_policy_layers : []) +
|
|
2082
|
+
(own_field_policy ? [own_field_policy] : [])
|
|
2083
|
+
@field_policy_layers.freeze
|
|
2084
|
+
# Data-field naming mode (see Parse::Agent::FieldNames). A sub-agent
|
|
2085
|
+
# inherits its parent's mode unless it sets one; naming never affects
|
|
2086
|
+
# the access restrictions inherited above.
|
|
2087
|
+
@field_names_mode = if field_names.nil?
|
|
2088
|
+
parent ? parent.field_names_mode : :default
|
|
2089
|
+
else
|
|
2090
|
+
Parse::AggregationResult.normalize_field_names!(field_names)
|
|
2091
|
+
end
|
|
2092
|
+
|
|
2093
|
+
# Sub-agent `tools:` inheritance: narrow only, same rule as `classes:`
|
|
2094
|
+
# below. Intersect onlies, union excepts. A child `only:` that leaves
|
|
2095
|
+
# nothing once the parent's allowlist and excepts apply raises; an
|
|
2096
|
+
# explicitly empty `only: []` is the strictest narrowing and is kept.
|
|
2097
|
+
if parent
|
|
2098
|
+
parent_tools_only = parent.instance_variable_get(:@tool_filter_only)
|
|
2099
|
+
parent_tools_except = parent.instance_variable_get(:@tool_filter_except)
|
|
2100
|
+
requested_tools = @tool_filter_only
|
|
2101
|
+
if parent_tools_only && @tool_filter_only
|
|
2102
|
+
@tool_filter_only = (@tool_filter_only & parent_tools_only).freeze
|
|
2103
|
+
elsif parent_tools_only
|
|
2104
|
+
@tool_filter_only = parent_tools_only
|
|
2105
|
+
end
|
|
2106
|
+
if parent_tools_except
|
|
2107
|
+
@tool_filter_except = (@tool_filter_except ? (@tool_filter_except | parent_tools_except) : parent_tools_except).freeze
|
|
2108
|
+
end
|
|
2109
|
+
if requested_tools && !requested_tools.empty? &&
|
|
2110
|
+
(parent_tools_only || parent_tools_except)
|
|
2111
|
+
reachable = @tool_filter_only - (@tool_filter_except || Set.new)
|
|
2112
|
+
if reachable.empty?
|
|
2113
|
+
raise ArgumentError,
|
|
2114
|
+
"sub-agent tools: { only: } would have no overlap with the parent's " \
|
|
2115
|
+
"tools: filter (parent only: #{parent_tools_only&.to_a&.sort.inspect}, " \
|
|
2116
|
+
"except: #{parent_tools_except&.to_a&.sort.inspect}; the child requested " \
|
|
2117
|
+
"#{requested_tools.to_a.sort.inspect}). A sub-agent cannot enable tools its " \
|
|
2118
|
+
"parent was not given. Pass a subset of the parent's tools, or omit tools: " \
|
|
2119
|
+
"to inherit the parent's filter."
|
|
2120
|
+
end
|
|
2121
|
+
end
|
|
2122
|
+
end
|
|
2036
2123
|
|
|
2037
|
-
# Sub-agent
|
|
2038
|
-
#
|
|
2124
|
+
# Sub-agent `methods:` inheritance. Entries can be bare (`:archive`) or
|
|
2125
|
+
# qualified (`"Post.archive"`), so plain set intersection would get the
|
|
2126
|
+
# matching wrong. Instead the parent's filters are kept as layers and
|
|
2127
|
+
# {#method_filtered?} refuses a call any layer refuses. A child
|
|
2128
|
+
# `only:` that no parent layer could permit raises; an explicitly
|
|
2129
|
+
# empty `only: []` is kept.
|
|
2130
|
+
@method_filter_layers = (parent ? parent.method_filter_layers : []).dup
|
|
2131
|
+
if parent && @method_filter_only && !@method_filter_only.empty? && !@method_filter_layers.empty?
|
|
2132
|
+
reachable = @method_filter_only.any? do |entry|
|
|
2133
|
+
@method_filter_layers.all? { |layer_only, layer_except| method_entry_reachable?(entry, layer_only, layer_except) }
|
|
2134
|
+
end
|
|
2135
|
+
unless reachable
|
|
2136
|
+
raise ArgumentError,
|
|
2137
|
+
"sub-agent methods: { only: } would have no overlap with the parent's " \
|
|
2138
|
+
"methods: filter (child requested #{@method_filter_only.to_a.map(&:to_s).sort.inspect}). " \
|
|
2139
|
+
"A sub-agent cannot call agent methods its parent was not given. Pass a " \
|
|
2140
|
+
"subset of the parent's methods, or omit methods: to inherit them."
|
|
2141
|
+
end
|
|
2142
|
+
end
|
|
2143
|
+
if @method_filter_only || @method_filter_except
|
|
2144
|
+
@method_filter_layers << [@method_filter_only, @method_filter_except].freeze
|
|
2145
|
+
end
|
|
2146
|
+
@method_filter_layers.freeze
|
|
2147
|
+
|
|
2148
|
+
# Sub-agent class-filter inheritance. Like `tools:` above,
|
|
2149
|
+
# `classes:` clamps to the parent's effective set so a
|
|
2039
2150
|
# sub-agent can NEVER widen its parent's data-reach. Intersect onlies,
|
|
2040
2151
|
# union excepts. A child `only:` that would have no overlap with the
|
|
2041
2152
|
# parent's effective set raises at construction — empty-onlyset means
|
|
@@ -2233,29 +2344,77 @@ module Parse
|
|
|
2233
2344
|
#
|
|
2234
2345
|
# An entry matches the invocation if it equals either the bare
|
|
2235
2346
|
# method name (`:archive`) or the qualified form (`"Class.archive"`).
|
|
2347
|
+
# The class part is compared by its Parse class name on both sides, so
|
|
2348
|
+
# `"User.reset"` and `"_User.reset"` name the same method whichever
|
|
2349
|
+
# spelling the entry or the caller used.
|
|
2236
2350
|
#
|
|
2237
2351
|
# @param method_name [Symbol, String]
|
|
2238
2352
|
# @param class_name [String]
|
|
2239
2353
|
# @return [Boolean] true if filtered (refuse), false if permitted
|
|
2240
2354
|
def method_filtered?(method_name, class_name:)
|
|
2241
|
-
return false if @method_filter_only.nil? && @method_filter_except.nil?
|
|
2242
|
-
|
|
2243
2355
|
method_sym = method_name.to_sym
|
|
2244
|
-
|
|
2356
|
+
parse_class = Parse::Agent.canonical_method_class(class_name)
|
|
2245
2357
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2358
|
+
# A sub-agent carries its parent's filters as earlier layers; its own
|
|
2359
|
+
# filter is the last layer. Any layer can refuse.
|
|
2360
|
+
method_filter_layers.any? do |only, except|
|
|
2361
|
+
(only && !method_entry_matches?(only, method_sym, parse_class)) ||
|
|
2362
|
+
(except && method_entry_matches?(except, method_sym, parse_class))
|
|
2250
2363
|
end
|
|
2364
|
+
end
|
|
2251
2365
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2366
|
+
# The Parse class name for the class part of a qualified `methods:`
|
|
2367
|
+
# entry or a `call_method` class argument: `"User"` and `"_User"`
|
|
2368
|
+
# both give `"_User"`. A name that resolves to no model is returned
|
|
2369
|
+
# unchanged.
|
|
2370
|
+
# @api private
|
|
2371
|
+
# @param name [String, Class]
|
|
2372
|
+
# @return [String]
|
|
2373
|
+
def self.canonical_method_class(name)
|
|
2374
|
+
return name.parse_class if name.is_a?(Class) && name.respond_to?(:parse_class)
|
|
2375
|
+
str = name.to_s
|
|
2376
|
+
klass = resolve_method_filter_class(str)
|
|
2377
|
+
klass ? klass.parse_class : str
|
|
2378
|
+
end
|
|
2257
2379
|
|
|
2258
|
-
|
|
2380
|
+
# Ruby constant names a `methods:` entry may use for a model whose Parse
|
|
2381
|
+
# class name differs (`class Artist < Parse::Object; parse_class
|
|
2382
|
+
# "Musician"; end`).
|
|
2383
|
+
RUBY_CONSTANT_NAME_RE = /\A[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/
|
|
2384
|
+
|
|
2385
|
+
# @!visibility private
|
|
2386
|
+
# The Parse::Object subclass a `methods:` class name refers to, by
|
|
2387
|
+
# Parse class name first and then by Ruby constant name, or nil.
|
|
2388
|
+
def self.resolve_method_filter_class(str)
|
|
2389
|
+
klass = begin
|
|
2390
|
+
Parse::Model.find_class(str)
|
|
2391
|
+
rescue StandardError
|
|
2392
|
+
nil
|
|
2393
|
+
end
|
|
2394
|
+
return klass if klass.is_a?(Class) && klass < Parse::Object
|
|
2395
|
+
return nil unless str.match?(RUBY_CONSTANT_NAME_RE)
|
|
2396
|
+
klass = begin
|
|
2397
|
+
Object.const_get(str)
|
|
2398
|
+
rescue StandardError, LoadError
|
|
2399
|
+
nil
|
|
2400
|
+
end
|
|
2401
|
+
klass.is_a?(Class) && klass < Parse::Object ? klass : nil
|
|
2402
|
+
end
|
|
2403
|
+
|
|
2404
|
+
# @return [Array<Array(Set, Set)>] the `methods:` filters in effect as
|
|
2405
|
+
# `[only, except]` pairs, own filter last; a sub-agent carries its
|
|
2406
|
+
# parent's layers first.
|
|
2407
|
+
def method_filter_layers
|
|
2408
|
+
@method_filter_layers || []
|
|
2409
|
+
end
|
|
2410
|
+
|
|
2411
|
+
# @return [Array<Hash>] {#method_filter_layers} as `{only:, except:}`
|
|
2412
|
+
# hashes of sorted name strings (nil when a layer has no such list),
|
|
2413
|
+
# parent layers first. Used by the audit payload and {#describe}.
|
|
2414
|
+
def method_filter_layers_descriptor
|
|
2415
|
+
method_filter_layers.map do |only, except|
|
|
2416
|
+
{ only: only && only.to_a.map(&:to_s).sort, except: except && except.to_a.map(&:to_s).sort }
|
|
2417
|
+
end
|
|
2259
2418
|
end
|
|
2260
2419
|
|
|
2261
2420
|
# @return [Boolean] whether unknown names in tools: raise vs. warn at
|
|
@@ -2487,6 +2646,15 @@ module Parse
|
|
|
2487
2646
|
payload[:tools_except] = @tool_filter_except.to_a.sort if @tool_filter_except
|
|
2488
2647
|
payload[:methods_only] = @method_filter_only.to_a.map(&:to_s).sort if @method_filter_only
|
|
2489
2648
|
payload[:methods_except] = @method_filter_except.to_a.map(&:to_s).sort if @method_filter_except
|
|
2649
|
+
# `methods_only` / `methods_except` are this agent's own filter. A
|
|
2650
|
+
# sub-agent is also bound by its parent's filters, so the full set in
|
|
2651
|
+
# force is emitted as layers (parent first) whenever any layer was
|
|
2652
|
+
# inherited, including a single parent layer on a child with no
|
|
2653
|
+
# filter of its own.
|
|
2654
|
+
own_method_layers = (@method_filter_only || @method_filter_except) ? 1 : 0
|
|
2655
|
+
if method_filter_layers.size > own_method_layers
|
|
2656
|
+
payload[:methods_layers] = method_filter_layers_descriptor
|
|
2657
|
+
end
|
|
2490
2658
|
# Per-agent per-class filters — emit class-name → field-name list,
|
|
2491
2659
|
# NOT the constraint values. Filter values can contain user-identifying
|
|
2492
2660
|
# data (`{ user_id: "abc123" }`, `{ org_id: tenant_uuid }`) that
|
|
@@ -3373,7 +3541,44 @@ module Parse
|
|
|
3373
3541
|
# strings (qualified-class.method match).
|
|
3374
3542
|
def normalize_method_filter_entry(value)
|
|
3375
3543
|
str = value.to_s
|
|
3376
|
-
str.include?(".")
|
|
3544
|
+
return str.to_sym unless str.include?(".")
|
|
3545
|
+
class_part, method_part = str.split(".", 2)
|
|
3546
|
+
if Parse::Agent.resolve_method_filter_class(class_part).nil?
|
|
3547
|
+
warn "[Parse::Agent] methods: entry #{str.inspect} names a class that does not " \
|
|
3548
|
+
"resolve to a loaded Parse::Object model (by Parse class name or Ruby " \
|
|
3549
|
+
"constant). It is matched by its class part as written; check the spelling."
|
|
3550
|
+
end
|
|
3551
|
+
"#{Parse::Agent.canonical_method_class(class_part)}.#{method_part}"
|
|
3552
|
+
end
|
|
3553
|
+
|
|
3554
|
+
# Whether a `methods:` filter set names this invocation, either bare or
|
|
3555
|
+
# qualified. Qualified entries are compared by Parse class name, which
|
|
3556
|
+
# also covers an entry whose class was not loaded at construction.
|
|
3557
|
+
def method_entry_matches?(set, method_sym, parse_class)
|
|
3558
|
+
return true if set.include?(method_sym) || set.include?("#{parse_class}.#{method_sym}")
|
|
3559
|
+
set.any? do |entry|
|
|
3560
|
+
next false unless entry.is_a?(String)
|
|
3561
|
+
class_part, method_part = entry.split(".", 2)
|
|
3562
|
+
method_part == method_sym.to_s && Parse::Agent.canonical_method_class(class_part) == parse_class
|
|
3563
|
+
end
|
|
3564
|
+
end
|
|
3565
|
+
|
|
3566
|
+
# Whether a child `methods:` only-entry could pass one parent layer.
|
|
3567
|
+
# A qualified entry ("Post.archive") passes when the layer names it or
|
|
3568
|
+
# its bare method. A bare entry (:archive) passes when the layer names
|
|
3569
|
+
# it bare or qualified on any class.
|
|
3570
|
+
def method_entry_reachable?(entry, layer_only, layer_except)
|
|
3571
|
+
if entry.is_a?(String)
|
|
3572
|
+
bare = entry.split(".", 2).last.to_sym
|
|
3573
|
+
return false if layer_except && (layer_except.include?(entry) || layer_except.include?(bare))
|
|
3574
|
+
return true unless layer_only
|
|
3575
|
+
layer_only.include?(entry) || layer_only.include?(bare)
|
|
3576
|
+
else
|
|
3577
|
+
return false if layer_except&.include?(entry)
|
|
3578
|
+
return true unless layer_only
|
|
3579
|
+
layer_only.include?(entry) ||
|
|
3580
|
+
layer_only.any? { |e| e.is_a?(String) && e.split(".", 2).last == entry.to_s }
|
|
3581
|
+
end
|
|
3377
3582
|
end
|
|
3378
3583
|
|
|
3379
3584
|
# Normalize the constructor's `classes:` kwarg into a [only_set,
|
|
@@ -3428,6 +3633,49 @@ module Parse
|
|
|
3428
3633
|
# lookup re-expands the variants and accepts both forms symmetrically.
|
|
3429
3634
|
#
|
|
3430
3635
|
# @return [Hash, nil] frozen Hash or nil when no filters declared
|
|
3636
|
+
# Normalize the constructor's `fields:` kwarg: a Hash mapping class
|
|
3637
|
+
# identifiers (Class, String, Symbol, or :default) to an Array of field
|
|
3638
|
+
# names. Keys canonicalize like `filters:` so `Parse::User` matches both
|
|
3639
|
+
# "_User" and "User". Field names may be Ruby property names or wire
|
|
3640
|
+
# names; they are translated to wire form at lookup time.
|
|
3641
|
+
def normalize_field_policy(fields)
|
|
3642
|
+
return nil if fields.nil?
|
|
3643
|
+
unless fields.is_a?(Hash)
|
|
3644
|
+
raise ArgumentError,
|
|
3645
|
+
"fields: must be a Hash mapping class identifiers (or :default) " \
|
|
3646
|
+
"to Arrays of field names, got #{fields.class}"
|
|
3647
|
+
end
|
|
3648
|
+
result = {}
|
|
3649
|
+
fields.each do |key, names|
|
|
3650
|
+
unless names.is_a?(Array) && names.all? { |n| n.is_a?(String) || n.is_a?(Symbol) }
|
|
3651
|
+
raise ArgumentError,
|
|
3652
|
+
"fields[#{key.inspect}]: value must be an Array of field names, got #{names.inspect}"
|
|
3653
|
+
end
|
|
3654
|
+
field_policy_keys(key).each { |canon| result[canon] = names.map(&:to_s).freeze }
|
|
3655
|
+
end
|
|
3656
|
+
result.freeze
|
|
3657
|
+
end
|
|
3658
|
+
|
|
3659
|
+
# Canonical lookup names for a `fields:` key. Tools look policies up by
|
|
3660
|
+
# the class's Parse name (`_User`, or a custom `parse_class`), so a
|
|
3661
|
+
# String or Symbol key is resolved through Parse::Model.find_class to
|
|
3662
|
+
# that name as well as kept verbatim; otherwise `fields: { "User" => ... }`
|
|
3663
|
+
# would silently fail to narrow `_User`.
|
|
3664
|
+
def field_policy_keys(key)
|
|
3665
|
+
keys = canonical_filter_key(key)
|
|
3666
|
+
if key.is_a?(String) || key.is_a?(Symbol)
|
|
3667
|
+
klass = (Parse::Model.find_class(key.to_s) rescue nil)
|
|
3668
|
+
if klass.respond_to?(:parse_class)
|
|
3669
|
+
keys |= [klass.parse_class.to_s]
|
|
3670
|
+
if defined?(Parse::Agent::MetadataRegistry) &&
|
|
3671
|
+
Parse::Agent::MetadataRegistry.respond_to?(:hidden_name_variants_for)
|
|
3672
|
+
keys |= Parse::Agent::MetadataRegistry.hidden_name_variants_for(klass)
|
|
3673
|
+
end
|
|
3674
|
+
end
|
|
3675
|
+
end
|
|
3676
|
+
keys
|
|
3677
|
+
end
|
|
3678
|
+
|
|
3431
3679
|
def normalize_query_filters(filters)
|
|
3432
3680
|
return nil if filters.nil?
|
|
3433
3681
|
unless filters.is_a?(Hash)
|
|
@@ -3567,6 +3815,35 @@ module Parse
|
|
|
3567
3815
|
#
|
|
3568
3816
|
# @param class_name [String, Symbol, Class]
|
|
3569
3817
|
# @return [Boolean]
|
|
3818
|
+
# @return [Array<Hash>] the per-agent `fields:` policies in effect, own
|
|
3819
|
+
# policy last; a sub-agent carries its parent's layers first. Each
|
|
3820
|
+
# layer maps a canonical class name (or :default) to raw field names.
|
|
3821
|
+
attr_reader :field_policy_layers
|
|
3822
|
+
|
|
3823
|
+
# @return [Symbol] `:default` or `:server`; see {Parse::Agent::FieldNames}.
|
|
3824
|
+
attr_reader :field_names_mode
|
|
3825
|
+
|
|
3826
|
+
# Wire-format field names this agent narrows `class_name` to, or nil when
|
|
3827
|
+
# no `fields:` layer names the class (or a :default). Every layer that
|
|
3828
|
+
# applies must permit a field, so a sub-agent can never widen its
|
|
3829
|
+
# parent. The result is intersected with the class's `agent_fields`
|
|
3830
|
+
# ceiling by {Parse::Agent::MetadataRegistry.field_allowlist}.
|
|
3831
|
+
#
|
|
3832
|
+
# @param class_name [String, Class]
|
|
3833
|
+
# @return [Array<String>, nil]
|
|
3834
|
+
def field_narrowing_for(class_name)
|
|
3835
|
+
return nil if @field_policy_layers.nil? || @field_policy_layers.empty?
|
|
3836
|
+
cn = class_name.respond_to?(:parse_class) ? class_name.parse_class.to_s : class_name.to_s
|
|
3837
|
+
result = nil
|
|
3838
|
+
@field_policy_layers.each do |layer|
|
|
3839
|
+
names = layer[cn] || layer[:default]
|
|
3840
|
+
next if names.nil?
|
|
3841
|
+
wire = Parse::Agent::MetadataRegistry.wire_field_names(cn, names)
|
|
3842
|
+
result = result ? (result & wire) : wire
|
|
3843
|
+
end
|
|
3844
|
+
result
|
|
3845
|
+
end
|
|
3846
|
+
|
|
3570
3847
|
def class_filter_permits?(class_name)
|
|
3571
3848
|
return true if @class_filter_only.nil? && @class_filter_except.nil?
|
|
3572
3849
|
candidates = class_name_variants_for(class_name)
|
data/lib/parse/api/batch.rb
CHANGED
|
@@ -26,7 +26,16 @@ module Parse
|
|
|
26
26
|
batch_operations = Parse::BatchOperation.new batch_operations
|
|
27
27
|
end
|
|
28
28
|
response = request(:post, "batch", body: batch_operations.as_json)
|
|
29
|
-
response.success? && response.batch?
|
|
29
|
+
return response.batch_responses if response.success? && response.batch?
|
|
30
|
+
return response if response.error?
|
|
31
|
+
# A successful HTTP response whose body is not an array of results
|
|
32
|
+
# cannot be matched to the submitted requests. Report it as a failure
|
|
33
|
+
# rather than as a batch where every write landed.
|
|
34
|
+
Parse::Response.error_response(
|
|
35
|
+
Parse::Response::ERROR_INTERNAL,
|
|
36
|
+
"Malformed batch response: expected an array of results",
|
|
37
|
+
http_status: response.http_status,
|
|
38
|
+
).tap { |r| r.request = response.request }
|
|
30
39
|
end
|
|
31
40
|
end
|
|
32
41
|
end
|
data/lib/parse/api/schema.rb
CHANGED
|
@@ -8,11 +8,19 @@ module Parse
|
|
|
8
8
|
# @!visibility private
|
|
9
9
|
SCHEMAS_PATH = "schemas"
|
|
10
10
|
|
|
11
|
+
# Parse Server serves `/schemas` to the master key only. Every call
|
|
12
|
+
# here therefore asks for the master key explicitly (outside client
|
|
13
|
+
# mode), which also stops an ambient `Parse.with_session` token or a
|
|
14
|
+
# client-bound token from being attached in its place. With a session token attached the request was
|
|
15
|
+
# refused with 403, and callers that cache the class permissions (the
|
|
16
|
+
# CLP scope used by mongo-direct queries) then denied a direct query
|
|
17
|
+
# that the master-keyed client was entitled to run.
|
|
18
|
+
|
|
11
19
|
# Get all the schemas for the application.
|
|
12
20
|
# @param opts [Hash] additional options for the request.
|
|
13
21
|
# @return [Parse::Response]
|
|
14
22
|
def schemas(opts = {})
|
|
15
|
-
request_opts = { cache: false }.merge(opts)
|
|
23
|
+
request_opts = { cache: false }.merge(schema_auth_opts).merge(opts)
|
|
16
24
|
request :get, SCHEMAS_PATH, opts: request_opts
|
|
17
25
|
end
|
|
18
26
|
|
|
@@ -21,7 +29,7 @@ module Parse
|
|
|
21
29
|
# @return [Parse::Response]
|
|
22
30
|
def schema(className)
|
|
23
31
|
safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
|
|
24
|
-
opts = { cache: false }
|
|
32
|
+
opts = { cache: false }.merge(schema_auth_opts)
|
|
25
33
|
request :get, "#{SCHEMAS_PATH}/#{safe}", opts: opts
|
|
26
34
|
end
|
|
27
35
|
|
|
@@ -32,7 +40,7 @@ module Parse
|
|
|
32
40
|
# @return [Parse::Response]
|
|
33
41
|
def create_schema(className, schema)
|
|
34
42
|
safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
|
|
35
|
-
request :post, "#{SCHEMAS_PATH}/#{safe}", body: schema
|
|
43
|
+
request :post, "#{SCHEMAS_PATH}/#{safe}", body: schema, opts: schema_auth_opts
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# Update the schema for a collection.
|
|
@@ -42,7 +50,18 @@ module Parse
|
|
|
42
50
|
# @return [Parse::Response]
|
|
43
51
|
def update_schema(className, schema)
|
|
44
52
|
safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
|
|
45
|
-
request :put, "#{SCHEMAS_PATH}/#{safe}", body: schema
|
|
53
|
+
request :put, "#{SCHEMAS_PATH}/#{safe}", body: schema, opts: schema_auth_opts
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Ask for the master key explicitly, except under `Parse.client_mode`,
|
|
59
|
+
# whose contract is that the SDK never sends the master key on its own
|
|
60
|
+
# initiative. A client with no master key configured sends none either
|
|
61
|
+
# way.
|
|
62
|
+
# @!visibility private
|
|
63
|
+
def schema_auth_opts
|
|
64
|
+
{ use_master_key: !Parse.client_mode }
|
|
46
65
|
end
|
|
47
66
|
end #Schema
|
|
48
67
|
end #API
|
data/lib/parse/api/sessions.rb
CHANGED
|
@@ -13,8 +13,12 @@ module Parse
|
|
|
13
13
|
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
14
14
|
# @return [Parse::Response]
|
|
15
15
|
def fetch_session(session_token, **opts)
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# The positional token is passed as the explicit per-call token so an
|
|
17
|
+
# ambient `Parse.with_session` token or a client-bound token cannot
|
|
18
|
+
# replace it and return another user's session.
|
|
19
|
+
session_token = session_token.session_token if session_token.respond_to?(:session_token)
|
|
20
|
+
opts = opts.merge({ use_master_key: false, cache: false, session_token: session_token.to_s })
|
|
21
|
+
headers = { Parse::Protocol::SESSION_TOKEN => session_token.to_s }
|
|
18
22
|
response = request :get, "#{SESSION_PATH_PREFIX}/me", headers: headers, opts: opts
|
|
19
23
|
response.parse_class = Parse::Model::CLASS_SESSION
|
|
20
24
|
response
|