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/agent.rb
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
require "active_support/notifications"
|
|
5
5
|
require "securerandom"
|
|
6
|
+
require "digest"
|
|
6
7
|
require "set"
|
|
7
8
|
require "uri"
|
|
9
|
+
require "monitor"
|
|
8
10
|
require_relative "mongodb"
|
|
9
11
|
require_relative "acl_scope"
|
|
10
12
|
require_relative "model/acl"
|
|
@@ -906,12 +908,45 @@ module Parse
|
|
|
906
908
|
attr_reader :acl_role_scope
|
|
907
909
|
|
|
908
910
|
# @return [Parse::ACLScope::Resolution, nil] the resolved ACL scope
|
|
909
|
-
# for this agent.
|
|
910
|
-
#
|
|
911
|
-
#
|
|
912
|
-
#
|
|
913
|
-
#
|
|
914
|
-
|
|
911
|
+
# for this agent. `nil` means master-key posture: the agent runs
|
|
912
|
+
# every tool call with the application master key, bypassing per-row
|
|
913
|
+
# ACL/CLP enforcement. Non-nil carries a `permission_strings`
|
|
914
|
+
# allow-set that built-in tools forward to mongo-direct / Atlas
|
|
915
|
+
# Search via {#acl_scope_kwargs}.
|
|
916
|
+
#
|
|
917
|
+
# A session-token agent is resolved eagerly at construction. When
|
|
918
|
+
# that fails (Parse Server unreachable), or after {#impersonate}, the
|
|
919
|
+
# scope is resolved again here on first use. It never reads as `nil`
|
|
920
|
+
# for a session agent: if the token still cannot be resolved this
|
|
921
|
+
# raises {Parse::Agent::UnresolvedIdentity} rather than letting a
|
|
922
|
+
# caller mistake the agent for master-key posture.
|
|
923
|
+
# @raise [Parse::Agent::UnresolvedIdentity]
|
|
924
|
+
def acl_scope
|
|
925
|
+
# The unresolved check and the read happen under one lock, so a
|
|
926
|
+
# concurrent #impersonate cannot clear the scope between them and
|
|
927
|
+
# hand a session agent a nil (master-looking) scope. Resolution itself
|
|
928
|
+
# runs outside the lock (see #ensure_session_scope!).
|
|
929
|
+
resolved = scope_mutex.synchronize do
|
|
930
|
+
session_scope_unresolved? ? :unresolved : @acl_scope
|
|
931
|
+
end
|
|
932
|
+
resolved == :unresolved ? ensure_session_scope! : resolved
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# @return [Boolean] `true` when the agent has a session token whose
|
|
936
|
+
# claim set has not been resolved yet (a failed eager resolution at
|
|
937
|
+
# construction, or a fresh {#impersonate}).
|
|
938
|
+
def session_scope_unresolved?
|
|
939
|
+
@acl_scope.nil? && !@session_token.to_s.empty?
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# The session token and its resolved scope read together under the scope
|
|
943
|
+
# lock, so a sub-agent never pairs one token with another token's scope.
|
|
944
|
+
# @return [Array(String, Parse::ACLScope::Resolution)] `[token, scope]`;
|
|
945
|
+
# either may be nil.
|
|
946
|
+
def scope_snapshot
|
|
947
|
+
scope_mutex.synchronize { [@session_token, @acl_scope] }
|
|
948
|
+
end
|
|
949
|
+
protected :scope_snapshot
|
|
915
950
|
|
|
916
951
|
# @return [Boolean] whether this agent may run Atlas Search tools
|
|
917
952
|
# in master-key-equivalent mode when no `session_token` is set.
|
|
@@ -1035,11 +1070,35 @@ module Parse
|
|
|
1035
1070
|
# `progress_callback` / `cancellation_token` are threaded. An
|
|
1036
1071
|
# embedder on the non-MCP path may assign any object responding to
|
|
1037
1072
|
# `#review`.
|
|
1073
|
+
#
|
|
1074
|
+
# A sub-agent built with `parent:` and no gate of its own uses its
|
|
1075
|
+
# parent's gate, read at call time: the dispatcher swaps the
|
|
1076
|
+
# parent's gate per request, so a copy taken at construction would
|
|
1077
|
+
# go stale. Without this a sub-agent fell back to {NullGate} and
|
|
1078
|
+
# ran tools in `require_approval_for` tiers with no approval.
|
|
1038
1079
|
def approval_gate
|
|
1039
|
-
@approval_gate
|
|
1080
|
+
return @approval_gate if @approval_gate
|
|
1081
|
+
return @approval_parent.approval_gate if @approval_parent
|
|
1082
|
+
@approval_gate_defaulted = true
|
|
1083
|
+
@approval_gate = Parse::Agent::NullGate.new
|
|
1040
1084
|
end
|
|
1041
1085
|
|
|
1042
|
-
|
|
1086
|
+
# Assign this agent's own approval gate. Assigning nil removes it, so
|
|
1087
|
+
# a sub-agent goes back to using its parent's gate.
|
|
1088
|
+
def approval_gate=(gate)
|
|
1089
|
+
@approval_gate_defaulted = false
|
|
1090
|
+
@approval_gate = gate
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
# @return [Parse::Agent::ApprovalGate, nil] the gate assigned on this
|
|
1094
|
+
# agent itself, or nil when it uses its parent's (or the default).
|
|
1095
|
+
# The MCP dispatcher snapshots and restores this rather than
|
|
1096
|
+
# {#approval_gate}, so restoring never pins a parent's gate.
|
|
1097
|
+
# @api private
|
|
1098
|
+
def approval_gate_override
|
|
1099
|
+
gate = @approval_gate
|
|
1100
|
+
gate.is_a?(Parse::Agent::NullGate) && @approval_gate_defaulted ? nil : gate
|
|
1101
|
+
end
|
|
1043
1102
|
|
|
1044
1103
|
# @return [Boolean] true if the active cancellation token has been
|
|
1045
1104
|
# tripped; false otherwise. Returns false when no token is
|
|
@@ -1145,7 +1204,11 @@ module Parse
|
|
|
1145
1204
|
#
|
|
1146
1205
|
# @return [Array<String>, nil]
|
|
1147
1206
|
def acl_permission_strings
|
|
1148
|
-
|
|
1207
|
+
scope = acl_scope
|
|
1208
|
+
# A session agent never reads as master-key posture, even if a scope
|
|
1209
|
+
# was cleared by something other than #impersonate.
|
|
1210
|
+
raise unresolved_identity_error if scope.nil? && !@session_token.to_s.empty?
|
|
1211
|
+
scope&.permission_strings
|
|
1149
1212
|
end
|
|
1150
1213
|
|
|
1151
1214
|
# A ready-to-prepend `$match` stage filtering an aggregation
|
|
@@ -1177,7 +1240,8 @@ module Parse
|
|
|
1177
1240
|
end
|
|
1178
1241
|
|
|
1179
1242
|
# `true` when the agent carries any non-master-key scope
|
|
1180
|
-
# (session_token, acl_user, or acl_role). Use this
|
|
1243
|
+
# (session_token, acl_user, or acl_role), resolved or not. Use this
|
|
1244
|
+
# when deciding
|
|
1181
1245
|
# whether a Parse Server endpoint that DOES NOT enforce ACL
|
|
1182
1246
|
# (notably the REST `aggregate` endpoint) is safe to route through:
|
|
1183
1247
|
# any `true` here means the REST path would silently bypass the
|
|
@@ -1186,7 +1250,7 @@ module Parse
|
|
|
1186
1250
|
#
|
|
1187
1251
|
# @return [Boolean]
|
|
1188
1252
|
def acl_scope?
|
|
1189
|
-
!@acl_scope.nil?
|
|
1253
|
+
!@acl_scope.nil? || !@session_token.to_s.empty?
|
|
1190
1254
|
end
|
|
1191
1255
|
|
|
1192
1256
|
# `true` when the agent's ACL scope cannot be honored by Parse
|
|
@@ -1237,16 +1301,25 @@ module Parse
|
|
|
1237
1301
|
#
|
|
1238
1302
|
# @return [Parse::ACLScope::Resolution, nil]
|
|
1239
1303
|
def refresh_scope!
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1304
|
+
# A session agent resolves (or raises) rather than returning nil,
|
|
1305
|
+
# which callers read as master-key posture.
|
|
1306
|
+
return acl_scope if @session_token
|
|
1307
|
+
user_scope, role_scope = scope_mutex.synchronize { [@acl_user_scope, @acl_role_scope] }
|
|
1308
|
+
return nil if user_scope.nil? && role_scope.nil?
|
|
1309
|
+
resolved = if user_scope
|
|
1310
|
+
Parse::ACLScope.resolve_for_user(user_scope, client: @client)
|
|
1244
1311
|
else
|
|
1245
|
-
Parse::ACLScope.resolve_for_role(
|
|
1312
|
+
Parse::ACLScope.resolve_for_role(role_scope, client: @client)
|
|
1313
|
+
end
|
|
1314
|
+
scope_mutex.synchronize do
|
|
1315
|
+
# Bind only if the identity did not change while resolving (a
|
|
1316
|
+
# concurrent #impersonate replaces it).
|
|
1317
|
+
if @session_token.nil? && @acl_user_scope.equal?(user_scope) && @acl_role_scope.equal?(role_scope)
|
|
1318
|
+
@acl_scope = resolved&.freeze
|
|
1319
|
+
@auth_context = nil # user_id may have changed
|
|
1246
1320
|
end
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
@acl_scope
|
|
1321
|
+
@acl_scope
|
|
1322
|
+
end
|
|
1250
1323
|
end
|
|
1251
1324
|
|
|
1252
1325
|
# @return [String, nil] free-form audit label attached to an
|
|
@@ -1269,30 +1342,42 @@ module Parse
|
|
|
1269
1342
|
# @return [self]
|
|
1270
1343
|
def impersonate(user, mint: false, label: nil)
|
|
1271
1344
|
token = resolve_impersonation_token!(user, mint: mint)
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1345
|
+
scope_mutex.synchronize do
|
|
1346
|
+
@session_token = token
|
|
1347
|
+
@acl_user_scope = nil
|
|
1348
|
+
@acl_role_scope = nil
|
|
1349
|
+
@impersonation_label = sanitize_impersonation_label(label) if label
|
|
1350
|
+
# Drop memoized scope/auth so the next call resolves under the new
|
|
1351
|
+
# token (session-token validity is checked per-call by Parse Server).
|
|
1352
|
+
@acl_scope = nil
|
|
1353
|
+
@auth_context = nil
|
|
1354
|
+
@scope_failed_at = nil
|
|
1355
|
+
@scope_failed_token = nil
|
|
1356
|
+
no_master_key = @client.respond_to?(:master_key) && @client.master_key.nil?
|
|
1357
|
+
@client_mode = no_master_key && !@session_token.to_s.empty?
|
|
1358
|
+
end
|
|
1282
1359
|
self
|
|
1283
1360
|
end
|
|
1284
1361
|
|
|
1285
|
-
# Clear an impersonation binding established via {#impersonate}
|
|
1286
|
-
# returning the agent to
|
|
1287
|
-
# underlying _Session row (the
|
|
1362
|
+
# Clear an impersonation binding established via {#impersonate} (or the
|
|
1363
|
+
# `impersonate_user:` constructor option), returning the agent to
|
|
1364
|
+
# master-key posture. Does not revoke the underlying _Session row (the
|
|
1365
|
+
# token may be shared/minted elsewhere). A no-op on an agent that is not
|
|
1366
|
+
# impersonating: an agent built with its own `session_token:` keeps it,
|
|
1367
|
+
# rather than silently gaining master-key posture.
|
|
1288
1368
|
# @return [self]
|
|
1289
1369
|
def stop_impersonating!
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1370
|
+
scope_mutex.synchronize do
|
|
1371
|
+
return self if @impersonated_user_id.nil?
|
|
1372
|
+
@session_token = nil
|
|
1373
|
+
@impersonated_user_id = nil
|
|
1374
|
+
@impersonation_label = nil
|
|
1375
|
+
@acl_scope = nil
|
|
1376
|
+
@auth_context = nil
|
|
1377
|
+
@scope_failed_at = nil
|
|
1378
|
+
@scope_failed_token = nil
|
|
1379
|
+
@client_mode = false
|
|
1380
|
+
end
|
|
1296
1381
|
self
|
|
1297
1382
|
end
|
|
1298
1383
|
|
|
@@ -1736,8 +1821,10 @@ module Parse
|
|
|
1736
1821
|
#
|
|
1737
1822
|
# * nil — inherit from parent (the common case; the
|
|
1738
1823
|
# child wants whatever the parent had).
|
|
1739
|
-
# * true
|
|
1740
|
-
#
|
|
1824
|
+
# * true: keep faceted_search authority. Allowed only when
|
|
1825
|
+
# the parent has it: a child cannot turn on
|
|
1826
|
+
# master-key faceting under a scoped parent, which
|
|
1827
|
+
# would return every row's buckets and results.
|
|
1741
1828
|
# * false — explicit opt-OUT: the sub-agent should DROP
|
|
1742
1829
|
# faceted_search authority even if the parent
|
|
1743
1830
|
# had it. Previously `false` was the default
|
|
@@ -1753,6 +1840,13 @@ module Parse
|
|
|
1753
1840
|
# per-row ACL via Parse::ACLScope's `_rperm` match and do
|
|
1754
1841
|
# NOT consult master_atlas.
|
|
1755
1842
|
master_atlas = parent.master_atlas if master_atlas.nil?
|
|
1843
|
+
if master_atlas == true && !parent.master_atlas
|
|
1844
|
+
raise ArgumentError,
|
|
1845
|
+
"sub-agent master_atlas: true exceeds parent's master_atlas: false. " \
|
|
1846
|
+
"A sub-agent cannot turn on master-key Atlas faceting its parent " \
|
|
1847
|
+
"was not given. Omit master_atlas to inherit the parent's setting, " \
|
|
1848
|
+
"or pass master_atlas: false."
|
|
1849
|
+
end
|
|
1756
1850
|
|
|
1757
1851
|
# Inherit cooperative cancellation surface. Without this, a
|
|
1758
1852
|
# delegating tool that constructs a sub-agent and drives it
|
|
@@ -1764,6 +1858,9 @@ module Parse
|
|
|
1764
1858
|
@cancellation_token = parent.cancellation_token
|
|
1765
1859
|
@progress_callback = parent.progress_callback
|
|
1766
1860
|
@log_callback = parent.log_callback
|
|
1861
|
+
# Approval gate: delegate to the parent's current gate (see
|
|
1862
|
+
# {#approval_gate}).
|
|
1863
|
+
@approval_parent = parent
|
|
1767
1864
|
|
|
1768
1865
|
# Clamp the sub-agent's permission tier at the parent's. The
|
|
1769
1866
|
# default :readonly is always ≤ any parent tier, so this fires
|
|
@@ -1809,6 +1906,13 @@ module Parse
|
|
|
1809
1906
|
@session_token = session_token
|
|
1810
1907
|
@acl_user_scope = acl_user
|
|
1811
1908
|
@acl_role_scope = acl_role
|
|
1909
|
+
# Guards the scope, the token, and the failed-resolution record, so a
|
|
1910
|
+
# concurrent #impersonate cannot leave one token's scope bound to
|
|
1911
|
+
# another, and concurrent first uses resolve only once.
|
|
1912
|
+
@scope_mutex = Monitor.new
|
|
1913
|
+
@scope_failed_at = nil
|
|
1914
|
+
@scope_failed_token = nil
|
|
1915
|
+
@scope_failure = nil
|
|
1812
1916
|
@tenant_id = tenant_id
|
|
1813
1917
|
@master_atlas = master_atlas == true
|
|
1814
1918
|
|
|
@@ -1902,18 +2006,24 @@ module Parse
|
|
|
1902
2006
|
@acl_scope = if @session_token
|
|
1903
2007
|
# Best-effort eager resolution. If Parse Server's /users/me is
|
|
1904
2008
|
# unreachable at construction time (network blip, test env, MCP
|
|
1905
|
-
# bootstrap-before-server-ready), leave
|
|
1906
|
-
#
|
|
1907
|
-
#
|
|
1908
|
-
#
|
|
1909
|
-
#
|
|
1910
|
-
#
|
|
1911
|
-
#
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2009
|
+
# bootstrap-before-server-ready), leave the scope unresolved:
|
|
2010
|
+
# {#acl_scope} retries on first use and raises
|
|
2011
|
+
# {Parse::Agent::UnresolvedIdentity} if it still fails, so an
|
|
2012
|
+
# unresolved session agent is never read as master-key posture.
|
|
2013
|
+
# Parse Server also validates the token on every REST call. The
|
|
2014
|
+
# banner check below keys on identity inputs, NOT on resolution
|
|
2015
|
+
# success, so an unresolved session_token does not trip the
|
|
2016
|
+
# master-key banner.
|
|
2017
|
+
#
|
|
2018
|
+
# A sub-agent holding its parent's own token on the parent's
|
|
2019
|
+
# client is the same identity: it reuses the parent's resolved
|
|
2020
|
+
# scope instead of a second /users/me call, and stays unresolved
|
|
2021
|
+
# (lazy path) when the parent has not resolved yet.
|
|
2022
|
+
parent_token, parent_scope = parent && @client.equal?(parent.client) ? parent.scope_snapshot : nil
|
|
2023
|
+
if parent_token && parent_token == @session_token
|
|
2024
|
+
parent_scope
|
|
2025
|
+
else
|
|
2026
|
+
eager_session_scope
|
|
1917
2027
|
end
|
|
1918
2028
|
elsif @acl_user_scope
|
|
1919
2029
|
Parse::ACLScope.resolve_for_user(@acl_user_scope, client: @client)
|
|
@@ -1939,10 +2049,26 @@ module Parse
|
|
|
1939
2049
|
# Same rationale.
|
|
1940
2050
|
# * Parent has explicit permission_strings → child MUST have a
|
|
1941
2051
|
# scope and child's permission_strings ⊆ parent's.
|
|
1942
|
-
|
|
1943
|
-
|
|
2052
|
+
# A parent whose session token has not resolved yet has an unknown
|
|
2053
|
+
# claim set. A child that inherits that same token is the same
|
|
2054
|
+
# identity and needs no check; any other child identity cannot be
|
|
2055
|
+
# proven a subset, so the parent's scope is resolved now (raising
|
|
2056
|
+
# Parse::Agent::UnresolvedIdentity if it still cannot be).
|
|
2057
|
+
parent_scope = if parent.nil?
|
|
2058
|
+
nil
|
|
2059
|
+
elsif parent.session_scope_unresolved? && @session_token == parent.session_token
|
|
2060
|
+
nil
|
|
2061
|
+
else
|
|
2062
|
+
parent.acl_scope
|
|
2063
|
+
end
|
|
2064
|
+
if parent_scope
|
|
2065
|
+
parent_perms = parent_scope.permission_strings
|
|
1944
2066
|
if parent_perms && !parent_perms.empty?
|
|
1945
2067
|
child_perms = @acl_scope&.permission_strings
|
|
2068
|
+
# A session token that did not resolve is not master-key posture:
|
|
2069
|
+
# its claim set is unknown, so the subset check cannot run. Report
|
|
2070
|
+
# it as unresolved rather than as an attempted widening.
|
|
2071
|
+
raise unresolved_identity_error if child_perms.nil? && session_scope_unresolved?
|
|
1946
2072
|
if child_perms.nil?
|
|
1947
2073
|
# SECURITY: emit the full diff on a dedicated audit
|
|
1948
2074
|
# channel; redact identifiers from the user-visible
|
|
@@ -2054,8 +2180,63 @@ module Parse
|
|
|
2054
2180
|
Parse::AggregationResult.normalize_field_names!(field_names)
|
|
2055
2181
|
end
|
|
2056
2182
|
|
|
2057
|
-
# Sub-agent
|
|
2058
|
-
#
|
|
2183
|
+
# Sub-agent `tools:` inheritance: narrow only, same rule as `classes:`
|
|
2184
|
+
# below. Intersect onlies, union excepts. A child `only:` that leaves
|
|
2185
|
+
# nothing once the parent's allowlist and excepts apply raises; an
|
|
2186
|
+
# explicitly empty `only: []` is the strictest narrowing and is kept.
|
|
2187
|
+
if parent
|
|
2188
|
+
parent_tools_only = parent.instance_variable_get(:@tool_filter_only)
|
|
2189
|
+
parent_tools_except = parent.instance_variable_get(:@tool_filter_except)
|
|
2190
|
+
requested_tools = @tool_filter_only
|
|
2191
|
+
if parent_tools_only && @tool_filter_only
|
|
2192
|
+
@tool_filter_only = (@tool_filter_only & parent_tools_only).freeze
|
|
2193
|
+
elsif parent_tools_only
|
|
2194
|
+
@tool_filter_only = parent_tools_only
|
|
2195
|
+
end
|
|
2196
|
+
if parent_tools_except
|
|
2197
|
+
@tool_filter_except = (@tool_filter_except ? (@tool_filter_except | parent_tools_except) : parent_tools_except).freeze
|
|
2198
|
+
end
|
|
2199
|
+
if requested_tools && !requested_tools.empty? &&
|
|
2200
|
+
(parent_tools_only || parent_tools_except)
|
|
2201
|
+
reachable = @tool_filter_only - (@tool_filter_except || Set.new)
|
|
2202
|
+
if reachable.empty?
|
|
2203
|
+
raise ArgumentError,
|
|
2204
|
+
"sub-agent tools: { only: } would have no overlap with the parent's " \
|
|
2205
|
+
"tools: filter (parent only: #{parent_tools_only&.to_a&.sort.inspect}, " \
|
|
2206
|
+
"except: #{parent_tools_except&.to_a&.sort.inspect}; the child requested " \
|
|
2207
|
+
"#{requested_tools.to_a.sort.inspect}). A sub-agent cannot enable tools its " \
|
|
2208
|
+
"parent was not given. Pass a subset of the parent's tools, or omit tools: " \
|
|
2209
|
+
"to inherit the parent's filter."
|
|
2210
|
+
end
|
|
2211
|
+
end
|
|
2212
|
+
end
|
|
2213
|
+
|
|
2214
|
+
# Sub-agent `methods:` inheritance. Entries can be bare (`:archive`) or
|
|
2215
|
+
# qualified (`"Post.archive"`), so plain set intersection would get the
|
|
2216
|
+
# matching wrong. Instead the parent's filters are kept as layers and
|
|
2217
|
+
# {#method_filtered?} refuses a call any layer refuses. A child
|
|
2218
|
+
# `only:` that no parent layer could permit raises; an explicitly
|
|
2219
|
+
# empty `only: []` is kept.
|
|
2220
|
+
@method_filter_layers = (parent ? parent.method_filter_layers : []).dup
|
|
2221
|
+
if parent && @method_filter_only && !@method_filter_only.empty? && !@method_filter_layers.empty?
|
|
2222
|
+
reachable = @method_filter_only.any? do |entry|
|
|
2223
|
+
@method_filter_layers.all? { |layer_only, layer_except| method_entry_reachable?(entry, layer_only, layer_except) }
|
|
2224
|
+
end
|
|
2225
|
+
unless reachable
|
|
2226
|
+
raise ArgumentError,
|
|
2227
|
+
"sub-agent methods: { only: } would have no overlap with the parent's " \
|
|
2228
|
+
"methods: filter (child requested #{@method_filter_only.to_a.map(&:to_s).sort.inspect}). " \
|
|
2229
|
+
"A sub-agent cannot call agent methods its parent was not given. Pass a " \
|
|
2230
|
+
"subset of the parent's methods, or omit methods: to inherit them."
|
|
2231
|
+
end
|
|
2232
|
+
end
|
|
2233
|
+
if @method_filter_only || @method_filter_except
|
|
2234
|
+
@method_filter_layers << [@method_filter_only, @method_filter_except].freeze
|
|
2235
|
+
end
|
|
2236
|
+
@method_filter_layers.freeze
|
|
2237
|
+
|
|
2238
|
+
# Sub-agent class-filter inheritance. Like `tools:` above,
|
|
2239
|
+
# `classes:` clamps to the parent's effective set so a
|
|
2059
2240
|
# sub-agent can NEVER widen its parent's data-reach. Intersect onlies,
|
|
2060
2241
|
# union excepts. A child `only:` that would have no overlap with the
|
|
2061
2242
|
# parent's effective set raises at construction — empty-onlyset means
|
|
@@ -2253,29 +2434,77 @@ module Parse
|
|
|
2253
2434
|
#
|
|
2254
2435
|
# An entry matches the invocation if it equals either the bare
|
|
2255
2436
|
# method name (`:archive`) or the qualified form (`"Class.archive"`).
|
|
2437
|
+
# The class part is compared by its Parse class name on both sides, so
|
|
2438
|
+
# `"User.reset"` and `"_User.reset"` name the same method whichever
|
|
2439
|
+
# spelling the entry or the caller used.
|
|
2256
2440
|
#
|
|
2257
2441
|
# @param method_name [Symbol, String]
|
|
2258
2442
|
# @param class_name [String]
|
|
2259
2443
|
# @return [Boolean] true if filtered (refuse), false if permitted
|
|
2260
2444
|
def method_filtered?(method_name, class_name:)
|
|
2261
|
-
return false if @method_filter_only.nil? && @method_filter_except.nil?
|
|
2262
|
-
|
|
2263
2445
|
method_sym = method_name.to_sym
|
|
2264
|
-
|
|
2446
|
+
parse_class = Parse::Agent.canonical_method_class(class_name)
|
|
2265
2447
|
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2448
|
+
# A sub-agent carries its parent's filters as earlier layers; its own
|
|
2449
|
+
# filter is the last layer. Any layer can refuse.
|
|
2450
|
+
method_filter_layers.any? do |only, except|
|
|
2451
|
+
(only && !method_entry_matches?(only, method_sym, parse_class)) ||
|
|
2452
|
+
(except && method_entry_matches?(except, method_sym, parse_class))
|
|
2270
2453
|
end
|
|
2454
|
+
end
|
|
2271
2455
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2456
|
+
# The Parse class name for the class part of a qualified `methods:`
|
|
2457
|
+
# entry or a `call_method` class argument: `"User"` and `"_User"`
|
|
2458
|
+
# both give `"_User"`. A name that resolves to no model is returned
|
|
2459
|
+
# unchanged.
|
|
2460
|
+
# @api private
|
|
2461
|
+
# @param name [String, Class]
|
|
2462
|
+
# @return [String]
|
|
2463
|
+
def self.canonical_method_class(name)
|
|
2464
|
+
return name.parse_class if name.is_a?(Class) && name.respond_to?(:parse_class)
|
|
2465
|
+
str = name.to_s
|
|
2466
|
+
klass = resolve_method_filter_class(str)
|
|
2467
|
+
klass ? klass.parse_class : str
|
|
2468
|
+
end
|
|
2277
2469
|
|
|
2278
|
-
|
|
2470
|
+
# Ruby constant names a `methods:` entry may use for a model whose Parse
|
|
2471
|
+
# class name differs (`class Artist < Parse::Object; parse_class
|
|
2472
|
+
# "Musician"; end`).
|
|
2473
|
+
RUBY_CONSTANT_NAME_RE = /\A[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/
|
|
2474
|
+
|
|
2475
|
+
# @!visibility private
|
|
2476
|
+
# The Parse::Object subclass a `methods:` class name refers to, by
|
|
2477
|
+
# Parse class name first and then by Ruby constant name, or nil.
|
|
2478
|
+
def self.resolve_method_filter_class(str)
|
|
2479
|
+
klass = begin
|
|
2480
|
+
Parse::Model.find_class(str)
|
|
2481
|
+
rescue StandardError
|
|
2482
|
+
nil
|
|
2483
|
+
end
|
|
2484
|
+
return klass if klass.is_a?(Class) && klass < Parse::Object
|
|
2485
|
+
return nil unless str.match?(RUBY_CONSTANT_NAME_RE)
|
|
2486
|
+
klass = begin
|
|
2487
|
+
Object.const_get(str)
|
|
2488
|
+
rescue StandardError, LoadError
|
|
2489
|
+
nil
|
|
2490
|
+
end
|
|
2491
|
+
klass.is_a?(Class) && klass < Parse::Object ? klass : nil
|
|
2492
|
+
end
|
|
2493
|
+
|
|
2494
|
+
# @return [Array<Array(Set, Set)>] the `methods:` filters in effect as
|
|
2495
|
+
# `[only, except]` pairs, own filter last; a sub-agent carries its
|
|
2496
|
+
# parent's layers first.
|
|
2497
|
+
def method_filter_layers
|
|
2498
|
+
@method_filter_layers || []
|
|
2499
|
+
end
|
|
2500
|
+
|
|
2501
|
+
# @return [Array<Hash>] {#method_filter_layers} as `{only:, except:}`
|
|
2502
|
+
# hashes of sorted name strings (nil when a layer has no such list),
|
|
2503
|
+
# parent layers first. Used by the audit payload and {#describe}.
|
|
2504
|
+
def method_filter_layers_descriptor
|
|
2505
|
+
method_filter_layers.map do |only, except|
|
|
2506
|
+
{ only: only && only.to_a.map(&:to_s).sort, except: except && except.to_a.map(&:to_s).sort }
|
|
2507
|
+
end
|
|
2279
2508
|
end
|
|
2280
2509
|
|
|
2281
2510
|
# @return [Boolean] whether unknown names in tools: raise vs. warn at
|
|
@@ -2507,6 +2736,15 @@ module Parse
|
|
|
2507
2736
|
payload[:tools_except] = @tool_filter_except.to_a.sort if @tool_filter_except
|
|
2508
2737
|
payload[:methods_only] = @method_filter_only.to_a.map(&:to_s).sort if @method_filter_only
|
|
2509
2738
|
payload[:methods_except] = @method_filter_except.to_a.map(&:to_s).sort if @method_filter_except
|
|
2739
|
+
# `methods_only` / `methods_except` are this agent's own filter. A
|
|
2740
|
+
# sub-agent is also bound by its parent's filters, so the full set in
|
|
2741
|
+
# force is emitted as layers (parent first) whenever any layer was
|
|
2742
|
+
# inherited, including a single parent layer on a child with no
|
|
2743
|
+
# filter of its own.
|
|
2744
|
+
own_method_layers = (@method_filter_only || @method_filter_except) ? 1 : 0
|
|
2745
|
+
if method_filter_layers.size > own_method_layers
|
|
2746
|
+
payload[:methods_layers] = method_filter_layers_descriptor
|
|
2747
|
+
end
|
|
2510
2748
|
# Per-agent per-class filters — emit class-name → field-name list,
|
|
2511
2749
|
# NOT the constraint values. Filter values can contain user-identifying
|
|
2512
2750
|
# data (`{ user_id: "abc123" }`, `{ org_id: tenant_uuid }`) that
|
|
@@ -3393,7 +3631,44 @@ module Parse
|
|
|
3393
3631
|
# strings (qualified-class.method match).
|
|
3394
3632
|
def normalize_method_filter_entry(value)
|
|
3395
3633
|
str = value.to_s
|
|
3396
|
-
str.include?(".")
|
|
3634
|
+
return str.to_sym unless str.include?(".")
|
|
3635
|
+
class_part, method_part = str.split(".", 2)
|
|
3636
|
+
if Parse::Agent.resolve_method_filter_class(class_part).nil?
|
|
3637
|
+
warn "[Parse::Agent] methods: entry #{str.inspect} names a class that does not " \
|
|
3638
|
+
"resolve to a loaded Parse::Object model (by Parse class name or Ruby " \
|
|
3639
|
+
"constant). It is matched by its class part as written; check the spelling."
|
|
3640
|
+
end
|
|
3641
|
+
"#{Parse::Agent.canonical_method_class(class_part)}.#{method_part}"
|
|
3642
|
+
end
|
|
3643
|
+
|
|
3644
|
+
# Whether a `methods:` filter set names this invocation, either bare or
|
|
3645
|
+
# qualified. Qualified entries are compared by Parse class name, which
|
|
3646
|
+
# also covers an entry whose class was not loaded at construction.
|
|
3647
|
+
def method_entry_matches?(set, method_sym, parse_class)
|
|
3648
|
+
return true if set.include?(method_sym) || set.include?("#{parse_class}.#{method_sym}")
|
|
3649
|
+
set.any? do |entry|
|
|
3650
|
+
next false unless entry.is_a?(String)
|
|
3651
|
+
class_part, method_part = entry.split(".", 2)
|
|
3652
|
+
method_part == method_sym.to_s && Parse::Agent.canonical_method_class(class_part) == parse_class
|
|
3653
|
+
end
|
|
3654
|
+
end
|
|
3655
|
+
|
|
3656
|
+
# Whether a child `methods:` only-entry could pass one parent layer.
|
|
3657
|
+
# A qualified entry ("Post.archive") passes when the layer names it or
|
|
3658
|
+
# its bare method. A bare entry (:archive) passes when the layer names
|
|
3659
|
+
# it bare or qualified on any class.
|
|
3660
|
+
def method_entry_reachable?(entry, layer_only, layer_except)
|
|
3661
|
+
if entry.is_a?(String)
|
|
3662
|
+
bare = entry.split(".", 2).last.to_sym
|
|
3663
|
+
return false if layer_except && (layer_except.include?(entry) || layer_except.include?(bare))
|
|
3664
|
+
return true unless layer_only
|
|
3665
|
+
layer_only.include?(entry) || layer_only.include?(bare)
|
|
3666
|
+
else
|
|
3667
|
+
return false if layer_except&.include?(entry)
|
|
3668
|
+
return true unless layer_only
|
|
3669
|
+
layer_only.include?(entry) ||
|
|
3670
|
+
layer_only.any? { |e| e.is_a?(String) && e.split(".", 2).last == entry.to_s }
|
|
3671
|
+
end
|
|
3397
3672
|
end
|
|
3398
3673
|
|
|
3399
3674
|
# Normalize the constructor's `classes:` kwarg into a [only_set,
|
|
@@ -3954,6 +4229,13 @@ module Parse
|
|
|
3954
4229
|
# acl_role, nil for master_key) so the AUDIT log can attribute
|
|
3955
4230
|
# tool calls accurately.
|
|
3956
4231
|
def auth_context
|
|
4232
|
+
# While the session is unresolved the identity is the token's
|
|
4233
|
+
# fingerprint (never the token), and the context is not memoized so
|
|
4234
|
+
# it picks up the user id once resolution succeeds.
|
|
4235
|
+
if session_scope_unresolved?
|
|
4236
|
+
return { type: :session_token, using_master_key: false,
|
|
4237
|
+
identity: "session:#{Digest::SHA256.hexdigest(@session_token.to_s)[0, 8]}" }
|
|
4238
|
+
end
|
|
3957
4239
|
@auth_context ||= if @session_token && !@session_token.to_s.empty?
|
|
3958
4240
|
{ type: :session_token, using_master_key: false,
|
|
3959
4241
|
identity: @acl_scope&.user_id }
|
|
@@ -3974,6 +4256,175 @@ module Parse
|
|
|
3974
4256
|
|
|
3975
4257
|
private
|
|
3976
4258
|
|
|
4259
|
+
public
|
|
4260
|
+
|
|
4261
|
+
# A redacted one-line summary. Never includes the session token, the
|
|
4262
|
+
# client (whose inspect would print its keys), the conversation, or the
|
|
4263
|
+
# last request and response: an agent is often logged or reported by an
|
|
4264
|
+
# error tracker, and the default Ruby inspect dumped all of them.
|
|
4265
|
+
# @return [String]
|
|
4266
|
+
def inspect
|
|
4267
|
+
parts = ["id=#{agent_id}", "permissions=#{@permissions}", "auth=#{auth_context[:type]}"]
|
|
4268
|
+
parts << "scope=unresolved" if session_scope_unresolved?
|
|
4269
|
+
parts << "parent=#{parent_agent_id}" if parent_agent_id
|
|
4270
|
+
parts << "depth=#{agent_depth}" if agent_depth.to_i.positive?
|
|
4271
|
+
parts << "tools_only=#{@tool_filter_only.size}" if @tool_filter_only
|
|
4272
|
+
parts << "tools_except=#{@tool_filter_except.size}" if @tool_filter_except
|
|
4273
|
+
parts << "classes_only=#{@class_filter_only.size}" if @class_filter_only
|
|
4274
|
+
parts << "classes_except=#{@class_filter_except.size}" if @class_filter_except
|
|
4275
|
+
parts << "method_layers=#{method_filter_layers.size}" unless method_filter_layers.empty?
|
|
4276
|
+
parts << "client_mode" if @client_mode
|
|
4277
|
+
"#<Parse::Agent #{parts.join(" ")}>"
|
|
4278
|
+
end
|
|
4279
|
+
alias_method :to_s, :inspect
|
|
4280
|
+
|
|
4281
|
+
# `pp` walks instance variables by default; print the redacted summary.
|
|
4282
|
+
def pretty_print(q)
|
|
4283
|
+
q.text(inspect)
|
|
4284
|
+
end
|
|
4285
|
+
|
|
4286
|
+
# ActiveSupport's default `as_json` serializes every instance variable,
|
|
4287
|
+
# including the session token and the client (with its keys). JSON log
|
|
4288
|
+
# formatters and error trackers call it, so return the same redacted
|
|
4289
|
+
# fields as {#inspect}.
|
|
4290
|
+
# @return [Hash]
|
|
4291
|
+
def as_json(*)
|
|
4292
|
+
{
|
|
4293
|
+
"agent_id" => agent_id,
|
|
4294
|
+
"permissions" => @permissions.to_s,
|
|
4295
|
+
"auth" => auth_context[:type].to_s,
|
|
4296
|
+
"scope_unresolved" => session_scope_unresolved?,
|
|
4297
|
+
"parent_agent_id" => parent_agent_id,
|
|
4298
|
+
"depth" => agent_depth,
|
|
4299
|
+
"client_mode" => @client_mode ? true : false,
|
|
4300
|
+
}.compact
|
|
4301
|
+
end
|
|
4302
|
+
|
|
4303
|
+
# @return [String] the redacted {#as_json} summary as JSON.
|
|
4304
|
+
def to_json(*args)
|
|
4305
|
+
as_json.to_json(*args)
|
|
4306
|
+
end
|
|
4307
|
+
|
|
4308
|
+
# YAML (Psych) serializes instance variables too; emit the redacted
|
|
4309
|
+
# summary instead.
|
|
4310
|
+
def encode_with(coder)
|
|
4311
|
+
as_json.each { |k, v| coder[k] = v }
|
|
4312
|
+
end
|
|
4313
|
+
|
|
4314
|
+
# An agent holds a session token and a client with its keys, so it is
|
|
4315
|
+
# never marshaled. Build a new agent from configuration instead.
|
|
4316
|
+
# @raise [TypeError]
|
|
4317
|
+
def marshal_dump
|
|
4318
|
+
raise TypeError, "Parse::Agent cannot be marshaled"
|
|
4319
|
+
end
|
|
4320
|
+
|
|
4321
|
+
private
|
|
4322
|
+
|
|
4323
|
+
# Seconds a failed lazy resolution is remembered before the next tool
|
|
4324
|
+
# call tries again. Inside the window calls are refused at once instead
|
|
4325
|
+
# of each waiting out a request to an unreachable Parse Server.
|
|
4326
|
+
SCOPE_RETRY_BACKOFF = 5
|
|
4327
|
+
|
|
4328
|
+
# Resolve a session token into a frozen ACL scope. Does network I/O and
|
|
4329
|
+
# touches no agent state, so it is safe to call without the scope lock.
|
|
4330
|
+
# @return [Array(Parse::ACLScope::Resolution, Symbol)] `[scope, nil]` on
|
|
4331
|
+
# success, `[nil, :invalid_session]` when Parse Server rejected the
|
|
4332
|
+
# token, `[nil, :unreachable]` otherwise.
|
|
4333
|
+
def resolve_session_scope(token)
|
|
4334
|
+
opts = { session_token: token, client: @client }.compact
|
|
4335
|
+
[Parse::ACLScope.resolve!(opts, method_name: :agent_init)&.freeze, nil]
|
|
4336
|
+
rescue Parse::Authorization::InvalidSession => e
|
|
4337
|
+
[nil, e.message.to_s.include?("lookup failed") ? :unreachable : :invalid_session]
|
|
4338
|
+
rescue StandardError
|
|
4339
|
+
[nil, :unreachable]
|
|
4340
|
+
end
|
|
4341
|
+
|
|
4342
|
+
# Best-effort resolution at construction. A failure is recorded like a
|
|
4343
|
+
# lazy one, so the first use inside the backoff window is refused at once
|
|
4344
|
+
# instead of paying a second lookup.
|
|
4345
|
+
def eager_session_scope
|
|
4346
|
+
token = @session_token
|
|
4347
|
+
scope, failure = resolve_session_scope(token)
|
|
4348
|
+
record_scope_failure(token, failure) if scope.nil?
|
|
4349
|
+
scope
|
|
4350
|
+
end
|
|
4351
|
+
|
|
4352
|
+
# Resolve a session token whose eager resolution failed. Raises instead
|
|
4353
|
+
# of leaving the scope nil, which every caller reads as master.
|
|
4354
|
+
#
|
|
4355
|
+
# The token is read under the scope lock, the lookup runs without it (so
|
|
4356
|
+
# other threads using the agent are not blocked for a request timeout),
|
|
4357
|
+
# and the result is bound under the lock only if the token is unchanged,
|
|
4358
|
+
# so a concurrent #impersonate never ends up with this token's scope.
|
|
4359
|
+
def ensure_session_scope!
|
|
4360
|
+
# A concurrent #impersonate can change the token while a lookup runs.
|
|
4361
|
+
# The stale result is dropped and the new identity resolved; a few
|
|
4362
|
+
# rounds bound this if impersonate keeps racing.
|
|
4363
|
+
3.times do
|
|
4364
|
+
token = scope_mutex.synchronize do
|
|
4365
|
+
return @acl_scope unless session_scope_unresolved?
|
|
4366
|
+
raise unresolved_identity_error if scope_failure_blocks?(@session_token)
|
|
4367
|
+
@session_token
|
|
4368
|
+
end
|
|
4369
|
+
resolved, failure = resolve_session_scope(token)
|
|
4370
|
+
bound = scope_mutex.synchronize do
|
|
4371
|
+
if @session_token != token
|
|
4372
|
+
:identity_changed
|
|
4373
|
+
elsif !@acl_scope.nil?
|
|
4374
|
+
# Another thread bound a scope for this token in the meantime.
|
|
4375
|
+
@acl_scope
|
|
4376
|
+
elsif resolved.nil?
|
|
4377
|
+
record_scope_failure(token, failure)
|
|
4378
|
+
raise unresolved_identity_error
|
|
4379
|
+
else
|
|
4380
|
+
@acl_scope = resolved
|
|
4381
|
+
@auth_context = nil
|
|
4382
|
+
@scope_failed_at = nil
|
|
4383
|
+
@scope_failed_token = nil
|
|
4384
|
+
@scope_failure = nil
|
|
4385
|
+
resolved
|
|
4386
|
+
end
|
|
4387
|
+
end
|
|
4388
|
+
return bound unless bound == :identity_changed
|
|
4389
|
+
end
|
|
4390
|
+
raise unresolved_identity_error
|
|
4391
|
+
end
|
|
4392
|
+
|
|
4393
|
+
def record_scope_failure(token, failure)
|
|
4394
|
+
scope_mutex.synchronize do
|
|
4395
|
+
@scope_failed_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
4396
|
+
@scope_failed_token = token
|
|
4397
|
+
@scope_failure = failure
|
|
4398
|
+
end
|
|
4399
|
+
end
|
|
4400
|
+
|
|
4401
|
+
# Whether a recorded failure for this token refuses the call without a
|
|
4402
|
+
# new lookup: always for a token Parse Server rejected (it will not
|
|
4403
|
+
# become valid), and within the backoff window for an unreachable server.
|
|
4404
|
+
def scope_failure_blocks?(token)
|
|
4405
|
+
return false if @scope_failed_at.nil? || @scope_failed_token != token
|
|
4406
|
+
return true if @scope_failure == :invalid_session
|
|
4407
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @scope_failed_at < SCOPE_RETRY_BACKOFF
|
|
4408
|
+
end
|
|
4409
|
+
|
|
4410
|
+
def unresolved_identity_error
|
|
4411
|
+
if @scope_failure == :invalid_session
|
|
4412
|
+
Parse::Agent::UnresolvedIdentity.new(
|
|
4413
|
+
"This agent's session token is invalid or expired, so its permissions " \
|
|
4414
|
+
"cannot be checked. The request was refused; rebuild the agent with a " \
|
|
4415
|
+
"valid session token.",
|
|
4416
|
+
)
|
|
4417
|
+
else
|
|
4418
|
+
Parse::Agent::UnresolvedIdentity.new
|
|
4419
|
+
end
|
|
4420
|
+
end
|
|
4421
|
+
|
|
4422
|
+
# Reentrant: #acl_scope holds it while #ensure_session_scope! takes it
|
|
4423
|
+
# again.
|
|
4424
|
+
def scope_mutex
|
|
4425
|
+
@scope_mutex ||= Monitor.new
|
|
4426
|
+
end
|
|
4427
|
+
|
|
3977
4428
|
# Keys that should never be logged for security reasons.
|
|
3978
4429
|
# Includes query-body keys (where, pipeline), credential keys (session_token,
|
|
3979
4430
|
# password, secret, token, auth_data, authData, recovery_codes, api_key,
|