parse-stack-next 5.6.0 → 5.7.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 +489 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +7 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +219 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/associations/belongs_to.rb +4 -0
- data/lib/parse/model/associations/collection_proxy.rb +3 -0
- data/lib/parse/model/associations/has_many.rb +4 -0
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +366 -113
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +21 -23
- data/lib/parse/model/core/fetching.rb +6 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +6 -2
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +4 -5
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +136 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +66 -41
- data/lib/parse/vector_search.rb +14 -11
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +120 -5
- data/parse-stack-next.gemspec +6 -6
- metadata +10 -1
data/lib/parse/acl_scope.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "set"
|
|
5
5
|
require_relative "model/acl"
|
|
6
6
|
require_relative "clp_scope"
|
|
7
|
+
require_relative "authorization"
|
|
7
8
|
|
|
8
9
|
module Parse
|
|
9
10
|
# Shared identity-resolution helper for query paths that simulate
|
|
@@ -22,11 +23,13 @@ module Parse
|
|
|
22
23
|
# userObjectId, "role:Admin", ...]`), so callers can prepend a
|
|
23
24
|
# `$match` stage built via {Parse::ACL.read_predicate}.
|
|
24
25
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
26
|
+
# Identity and role resolution live in {Parse::Authorization}, which
|
|
27
|
+
# owns the token → user_id → role-expansion pipeline and its caches.
|
|
28
|
+
# This module adds a path-agnostic kwarg-popping front door on top, so
|
|
29
|
+
# every mongo-direct entry point speaks the same auth vocabulary.
|
|
30
|
+
# Atlas Search is a peer consumer of the same resolver, not its home:
|
|
31
|
+
# `Parse::Query#results_direct` runs no `$search` at all and must not
|
|
32
|
+
# depend on the Atlas Search namespace to decide who the caller is.
|
|
30
33
|
module ACLScope
|
|
31
34
|
# Raised when a query path is configured to require an explicit
|
|
32
35
|
# session-token or master mode and the caller supplied neither.
|
|
@@ -50,7 +53,7 @@ module Parse
|
|
|
50
53
|
# @return [String, nil] the resolved user_id, or `nil` for
|
|
51
54
|
# `:master` and `:public`.
|
|
52
55
|
# @!attribute session
|
|
53
|
-
# @return [Parse::
|
|
56
|
+
# @return [Parse::Authorization::Resolved, nil] the
|
|
54
57
|
# underlying resolved-session struct (carries role-name set),
|
|
55
58
|
# `nil` for `:master`.
|
|
56
59
|
# @!attribute strict_role
|
|
@@ -64,13 +67,38 @@ module Parse
|
|
|
64
67
|
# `strict_role: true`, rows with NO `_rperm` field still pass
|
|
65
68
|
# (Parse-Server treats absent `_rperm` as public-default); the
|
|
66
69
|
# knob only suppresses the `"*"` subscription in the `$in` set.
|
|
67
|
-
|
|
70
|
+
# @!attribute client
|
|
71
|
+
# @return [Parse::Client, nil] the client whose authorization context
|
|
72
|
+
# produced this resolution. Carried so a caller about to run the
|
|
73
|
+
# query against a process-global resource can check that the two
|
|
74
|
+
# agree; see {Parse::MongoDB.verify_client!}. Per-client
|
|
75
|
+
# authorization plus a process-global MongoDB connection is safe only
|
|
76
|
+
# when they belong to the same application.
|
|
77
|
+
Resolution = Struct.new(:mode, :permission_strings, :user_id, :session, :strict_role, :client, keyword_init: true) do
|
|
68
78
|
def master?; mode == :master; end
|
|
69
79
|
def session?; mode == :session; end
|
|
70
80
|
def public?; mode == :public; end
|
|
71
81
|
def strict_role?; strict_role == true; end
|
|
72
82
|
end
|
|
73
83
|
|
|
84
|
+
# The client a resolution was produced by, or nil when it cannot say.
|
|
85
|
+
#
|
|
86
|
+
# Deliberately tolerant of objects that are resolution-shaped without
|
|
87
|
+
# being a {Resolution}: test doubles and any caller-supplied stand-in.
|
|
88
|
+
# A resolution that cannot name its client IS an unidentified caller,
|
|
89
|
+
# which is a case {Parse::MongoDB.verify_client!} models explicitly (it
|
|
90
|
+
# allows the single-application case and refuses the ambiguous one).
|
|
91
|
+
# Crashing on the missing method instead would convert an unknown into
|
|
92
|
+
# a NoMethodError far from its cause.
|
|
93
|
+
#
|
|
94
|
+
# @param resolution [Object, nil]
|
|
95
|
+
# @return [Parse::Client, nil]
|
|
96
|
+
def self.client_of(resolution)
|
|
97
|
+
return nil if resolution.nil?
|
|
98
|
+
return nil unless resolution.respond_to?(:client)
|
|
99
|
+
resolution.client
|
|
100
|
+
end
|
|
101
|
+
|
|
74
102
|
class << self
|
|
75
103
|
# When `true`, every call to {.resolve!} that did NOT receive
|
|
76
104
|
# `session_token:` or `master: true` raises {ACLRequired} instead
|
|
@@ -124,7 +152,7 @@ module Parse
|
|
|
124
152
|
# Parse::Query#scope_to_user. Mirrors the session-token path
|
|
125
153
|
# but skips the /users/me round-trip; role expansion still
|
|
126
154
|
# runs via Parse::Role.all_for_user.
|
|
127
|
-
return resolve_for_user(acl_user)
|
|
155
|
+
return resolve_for_user(acl_user, client: authorization_client(options))
|
|
128
156
|
end
|
|
129
157
|
|
|
130
158
|
if acl_role
|
|
@@ -137,22 +165,25 @@ module Parse
|
|
|
137
165
|
# identity. Parent-role inheritance applies (passing
|
|
138
166
|
# "scope:admin" includes any role "scope:admin" inherits
|
|
139
167
|
# from).
|
|
140
|
-
return resolve_for_role(acl_role, strict_role: strict_role
|
|
168
|
+
return resolve_for_role(acl_role, strict_role: strict_role,
|
|
169
|
+
client: authorization_client(options))
|
|
141
170
|
end
|
|
142
171
|
|
|
143
172
|
if session_token
|
|
144
|
-
|
|
145
|
-
resolved =
|
|
173
|
+
context = authorization_for(options)
|
|
174
|
+
resolved = context.resolve(session_token)
|
|
146
175
|
return Resolution.new(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
176
|
+
mode: :session,
|
|
177
|
+
permission_strings: resolved.permission_strings,
|
|
178
|
+
user_id: resolved.user_id,
|
|
179
|
+
session: resolved,
|
|
180
|
+
client: context.client,
|
|
181
|
+
)
|
|
152
182
|
end
|
|
153
183
|
|
|
154
184
|
if master == true
|
|
155
|
-
return Resolution.new(mode: :master, permission_strings: nil, user_id: nil, session: nil
|
|
185
|
+
return Resolution.new(mode: :master, permission_strings: nil, user_id: nil, session: nil,
|
|
186
|
+
client: authorization_client(options))
|
|
156
187
|
end
|
|
157
188
|
|
|
158
189
|
if @require_session_token == true
|
|
@@ -165,13 +196,13 @@ module Parse
|
|
|
165
196
|
end
|
|
166
197
|
|
|
167
198
|
warn_no_acl_context_once!(method_name)
|
|
168
|
-
|
|
169
|
-
anonymous = Parse::AtlasSearch::Session::Resolved.new(nil, Set.new)
|
|
199
|
+
anonymous = Parse::Authorization::Resolved.new(nil, Set.new)
|
|
170
200
|
Resolution.new(
|
|
171
201
|
mode: :public,
|
|
172
202
|
permission_strings: anonymous.permission_strings,
|
|
173
203
|
user_id: nil,
|
|
174
204
|
session: anonymous,
|
|
205
|
+
client: authorization_client(options),
|
|
175
206
|
)
|
|
176
207
|
end
|
|
177
208
|
|
|
@@ -384,8 +415,7 @@ module Parse
|
|
|
384
415
|
# either shape so the CLP gate fires before the String→Hash
|
|
385
416
|
# upgrade — denying access to the joined class BEFORE we go
|
|
386
417
|
# to the trouble of building out an upgraded sub-pipeline.
|
|
387
|
-
target =
|
|
388
|
-
if spec.is_a?(String)
|
|
418
|
+
target = if spec.is_a?(String)
|
|
389
419
|
spec
|
|
390
420
|
elsif spec.is_a?(Hash)
|
|
391
421
|
spec["coll"] || spec[:coll]
|
|
@@ -417,8 +447,7 @@ module Parse
|
|
|
417
447
|
# `$and`.
|
|
418
448
|
acl_predicate = acl_match["$match"]
|
|
419
449
|
existing = spec["restrictSearchWithMatch"] || spec[:restrictSearchWithMatch]
|
|
420
|
-
combined =
|
|
421
|
-
if existing.nil? || (existing.respond_to?(:empty?) && existing.empty?)
|
|
450
|
+
combined = if existing.nil? || (existing.respond_to?(:empty?) && existing.empty?)
|
|
422
451
|
acl_predicate
|
|
423
452
|
else
|
|
424
453
|
{ "$and" => [existing, acl_predicate] }
|
|
@@ -431,8 +460,7 @@ module Parse
|
|
|
431
460
|
def rewrite_facet(spec, acl_match, perms)
|
|
432
461
|
return spec unless spec.is_a?(Hash)
|
|
433
462
|
spec.each_with_object({}) do |(branch_name, branch_pipeline), out|
|
|
434
|
-
out[branch_name] =
|
|
435
|
-
if branch_pipeline.is_a?(Array)
|
|
463
|
+
out[branch_name] = if branch_pipeline.is_a?(Array)
|
|
436
464
|
# Recurse with the same perms — facet branches are
|
|
437
465
|
# evaluated in the requesting session's authority, not
|
|
438
466
|
# elevated.
|
|
@@ -565,7 +593,7 @@ module Parse
|
|
|
565
593
|
# through a session token.
|
|
566
594
|
# @param user [Parse::User, Parse::Pointer]
|
|
567
595
|
# @return [Resolution]
|
|
568
|
-
def resolve_for_user(user)
|
|
596
|
+
def resolve_for_user(user, client: nil)
|
|
569
597
|
# SECURITY: className must be `_User` (or the legacy `User`
|
|
570
598
|
# alias). Without this check, any duck-typed object exposing
|
|
571
599
|
# `#id` — including a `Parse::Pointer` to a foreign class
|
|
@@ -596,10 +624,13 @@ module Parse
|
|
|
596
624
|
"User Pointer with a non-empty objectId."
|
|
597
625
|
end
|
|
598
626
|
|
|
599
|
-
role_names =
|
|
600
|
-
begin
|
|
627
|
+
role_names = begin
|
|
601
628
|
require_relative "model/classes/role"
|
|
602
|
-
|
|
629
|
+
# `client:` matters here for the same reason it does on the
|
|
630
|
+
# session-token path: resolving the user against one application
|
|
631
|
+
# and then walking the role graph on another would grant that
|
|
632
|
+
# user the other application's roles by name.
|
|
633
|
+
Parse::Role.all_for_user(user, max_depth: 10, client: client)
|
|
603
634
|
rescue StandardError
|
|
604
635
|
Set.new
|
|
605
636
|
end
|
|
@@ -608,12 +639,12 @@ module Parse
|
|
|
608
639
|
role_names.each { |name| perms << "role:#{name}" if name && !name.empty? }
|
|
609
640
|
perms.uniq!
|
|
610
641
|
|
|
611
|
-
require_atlas_session!
|
|
612
642
|
Resolution.new(
|
|
613
643
|
mode: :session,
|
|
614
644
|
permission_strings: perms,
|
|
615
645
|
user_id: user.id,
|
|
616
|
-
session: Parse::
|
|
646
|
+
session: Parse::Authorization::Resolved.new(user.id, role_names),
|
|
647
|
+
client: client,
|
|
617
648
|
)
|
|
618
649
|
end
|
|
619
650
|
|
|
@@ -646,24 +677,22 @@ module Parse
|
|
|
646
677
|
# {.match_stage_for} for the precise predicate shape.
|
|
647
678
|
# @return [Resolution]
|
|
648
679
|
# @raise [ArgumentError] when the role cannot be resolved.
|
|
649
|
-
def resolve_for_role(role, strict_role: false)
|
|
680
|
+
def resolve_for_role(role, strict_role: false, client: nil)
|
|
650
681
|
require_relative "model/classes/role"
|
|
651
|
-
role_obj =
|
|
652
|
-
case role
|
|
682
|
+
role_obj = case role
|
|
653
683
|
when Parse::Role then role
|
|
654
684
|
when String, Symbol
|
|
655
685
|
name = role.to_s.sub(/\Arole:/, "")
|
|
656
686
|
raise ArgumentError, "[Parse::ACLScope] role name must be non-empty." if name.empty?
|
|
657
|
-
found =
|
|
687
|
+
found = role_lookup_by_name(name, client: client)
|
|
658
688
|
raise ArgumentError, "[Parse::ACLScope] no _Role found with name #{name.inspect}." if found.nil?
|
|
659
689
|
found
|
|
660
690
|
else
|
|
661
691
|
raise ArgumentError, "[Parse::ACLScope] resolve_for_role expects Parse::Role or String."
|
|
662
692
|
end
|
|
663
693
|
|
|
664
|
-
names =
|
|
665
|
-
|
|
666
|
-
role_obj.all_parent_role_names(max_depth: 10)
|
|
694
|
+
names = begin
|
|
695
|
+
role_obj.all_parent_role_names(max_depth: 10, client: client)
|
|
667
696
|
rescue StandardError
|
|
668
697
|
Set.new([role_obj.name].compact)
|
|
669
698
|
end
|
|
@@ -678,13 +707,13 @@ module Parse
|
|
|
678
707
|
names.each { |n| perms << "role:#{n}" if n && !n.empty? }
|
|
679
708
|
perms.uniq!
|
|
680
709
|
|
|
681
|
-
require_atlas_session!
|
|
682
710
|
Resolution.new(
|
|
683
711
|
mode: :session,
|
|
684
712
|
permission_strings: perms,
|
|
685
713
|
user_id: nil,
|
|
686
|
-
session: Parse::
|
|
714
|
+
session: Parse::Authorization::Resolved.new(nil, names),
|
|
687
715
|
strict_role: strict_role,
|
|
716
|
+
client: client,
|
|
688
717
|
)
|
|
689
718
|
end
|
|
690
719
|
|
|
@@ -717,18 +746,62 @@ module Parse
|
|
|
717
746
|
"= true to make this misuse an error instead of a warning."
|
|
718
747
|
end
|
|
719
748
|
|
|
720
|
-
#
|
|
721
|
-
#
|
|
722
|
-
# `
|
|
723
|
-
#
|
|
724
|
-
#
|
|
725
|
-
#
|
|
726
|
-
#
|
|
727
|
-
#
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
749
|
+
# The authorization context that should resolve this call.
|
|
750
|
+
#
|
|
751
|
+
# **Mutates `options`** by `delete`-ing `:client`, consistent with the
|
|
752
|
+
# other auth kwargs, so the remaining hash can be forwarded to the
|
|
753
|
+
# transport without leaking it.
|
|
754
|
+
#
|
|
755
|
+
# Defaults to `Parse.client` at this boundary. Below it,
|
|
756
|
+
# {Parse::Authorization.resolve} requires `client:` with no default: a
|
|
757
|
+
# token belonging to application B must never be validated against
|
|
758
|
+
# application A, which is exactly what a global resolver did.
|
|
759
|
+
#
|
|
760
|
+
# This used to lazily `require "atlas_search"` and call
|
|
761
|
+
# `Parse::AtlasSearch::Session.resolve`, so a plain mongo-direct query
|
|
762
|
+
# with no `$search` anywhere in it pulled in the Atlas Search namespace
|
|
763
|
+
# to decide who the caller was.
|
|
764
|
+
def authorization_for(options)
|
|
765
|
+
client = authorization_client(options)
|
|
766
|
+
if client.nil?
|
|
767
|
+
raise ACLRequired,
|
|
768
|
+
"Parse::ACLScope could not determine a Parse client to resolve this " \
|
|
769
|
+
"session token against. Pass client: explicitly, or configure a default " \
|
|
770
|
+
"client with Parse.setup."
|
|
771
|
+
end
|
|
772
|
+
client.authorization
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# Look up a `_Role` by name against a SPECIFIC client.
|
|
776
|
+
#
|
|
777
|
+
# `Parse::Role.first` resolves its client through the class, which is
|
|
778
|
+
# the default. On the `acl_role:` path that would find a role in the
|
|
779
|
+
# default application and then use its name to grant access in another,
|
|
780
|
+
# which is the same cross-application mixing the session-token path
|
|
781
|
+
# guards against. A nil client keeps the historical behavior.
|
|
782
|
+
def role_lookup_by_name(name, client: nil)
|
|
783
|
+
return Parse::Role.first(name: name) if client.nil?
|
|
784
|
+
query = Parse::Role.query(name: name)
|
|
785
|
+
query.client = client
|
|
786
|
+
query.first
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# The client for this call, or `nil` when none can be determined.
|
|
790
|
+
#
|
|
791
|
+
# **Mutates `options`** by `delete`-ing `:client`, so calling this more
|
|
792
|
+
# than once per call is safe: the first call consumes the kwarg and
|
|
793
|
+
# later ones fall back to the default.
|
|
794
|
+
#
|
|
795
|
+
# Never raises. The master and public paths run no identity resolution
|
|
796
|
+
# and historically worked in a process that had never called
|
|
797
|
+
# `Parse.setup`, so making them raise `ConnectionError` just to label
|
|
798
|
+
# the resolution would be a regression. They still want the label when
|
|
799
|
+
# one is available, because {Parse::MongoDB.verify_client!} checks it
|
|
800
|
+
# against the application the global connection is bound to.
|
|
801
|
+
def authorization_client(options)
|
|
802
|
+
options.delete(:client) || Parse.client
|
|
803
|
+
rescue Parse::Error::ConnectionError
|
|
804
|
+
nil
|
|
732
805
|
end
|
|
733
806
|
end
|
|
734
807
|
|
|
Binary file
|
|
@@ -44,14 +44,14 @@ module Parse
|
|
|
44
44
|
|
|
45
45
|
def initialize
|
|
46
46
|
@cancelled = false
|
|
47
|
-
@reason
|
|
47
|
+
@reason = nil
|
|
48
48
|
# Mutex protects the read-modify-write in {#cancel!} so a
|
|
49
49
|
# concurrent cancel from notifications/cancelled and client
|
|
50
50
|
# disconnect cannot lose a reason or partially update state.
|
|
51
51
|
# The hot poll path (#cancelled?) reads the boolean ivar
|
|
52
52
|
# directly — atomic on MRI and on each major Ruby
|
|
53
53
|
# implementation we ship against.
|
|
54
|
-
@mutex
|
|
54
|
+
@mutex = Mutex.new
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# @return [Boolean] true once {#cancel!} has been called at least once.
|
|
@@ -71,7 +71,7 @@ module Parse
|
|
|
71
71
|
@mutex.synchronize do
|
|
72
72
|
return false if @cancelled
|
|
73
73
|
@cancelled = true
|
|
74
|
-
@reason
|
|
74
|
+
@reason = reason
|
|
75
75
|
true
|
|
76
76
|
end
|
|
77
77
|
end
|
|
@@ -171,10 +171,10 @@ module Parse
|
|
|
171
171
|
# count deltas even when operators are otherwise clean.
|
|
172
172
|
assert_where_key_permitted!(key)
|
|
173
173
|
result[columnize(key)] = if key == "$relatedTo"
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
translate_related_to_value(value, depth: 0, agent: agent)
|
|
175
|
+
else
|
|
176
|
+
translate_value(value, depth: 0, agent: agent)
|
|
177
|
+
end
|
|
178
178
|
end
|
|
179
179
|
end
|
|
180
180
|
|
data/lib/parse/agent/describe.rb
CHANGED
|
@@ -51,14 +51,14 @@ module Parse
|
|
|
51
51
|
class_name.to_s
|
|
52
52
|
end
|
|
53
53
|
{
|
|
54
|
-
class_name:
|
|
55
|
-
accessible:
|
|
56
|
-
agent_fields:
|
|
57
|
-
agent_canonical_filter:
|
|
58
|
-
per_agent_filter:
|
|
59
|
-
tenant_scope:
|
|
60
|
-
large_fields:
|
|
61
|
-
agent_methods:
|
|
54
|
+
class_name: cn,
|
|
55
|
+
accessible: describe_class_accessibility(cn),
|
|
56
|
+
agent_fields: class_field_allowlist(cn),
|
|
57
|
+
agent_canonical_filter: Parse::Agent::MetadataRegistry.canonical_filter(cn),
|
|
58
|
+
per_agent_filter: respond_to?(:filter_for) ? filter_for(cn) : nil,
|
|
59
|
+
tenant_scope: class_tenant_scope(cn),
|
|
60
|
+
large_fields: class_large_fields(cn),
|
|
61
|
+
agent_methods: class_agent_method_names(cn),
|
|
62
62
|
}
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -136,7 +136,7 @@ module Parse
|
|
|
136
136
|
# mirroring assert_class_accessible!'s signature.
|
|
137
137
|
if class_name
|
|
138
138
|
cn = class_name.is_a?(Class) && class_name.respond_to?(:parse_class) ?
|
|
139
|
-
|
|
139
|
+
class_name.parse_class : class_name.to_s
|
|
140
140
|
begin
|
|
141
141
|
Parse::Agent::Tools.assert_class_accessible!(cn, agent: self, op: op)
|
|
142
142
|
rescue Parse::Agent::AccessDenied => e
|
|
@@ -152,7 +152,7 @@ module Parse
|
|
|
152
152
|
# tool is call_method (the method-filter only narrows that tool).
|
|
153
153
|
if tool_sym == :call_method && method_name && class_name
|
|
154
154
|
cn = class_name.is_a?(Class) && class_name.respond_to?(:parse_class) ?
|
|
155
|
-
|
|
155
|
+
class_name.parse_class : class_name.to_s
|
|
156
156
|
if respond_to?(:method_filtered?) &&
|
|
157
157
|
method_filtered?(method_name.to_sym, class_name: cn)
|
|
158
158
|
return { allowed: false, reason: :method_filtered,
|
|
@@ -169,23 +169,23 @@ module Parse
|
|
|
169
169
|
# paths share the same data source.
|
|
170
170
|
def describe_hash
|
|
171
171
|
{
|
|
172
|
-
agent_id:
|
|
173
|
-
agent_depth:
|
|
174
|
-
permissions:
|
|
175
|
-
auth:
|
|
176
|
-
tenant_id:
|
|
177
|
-
classes:
|
|
178
|
-
tools:
|
|
179
|
-
methods:
|
|
180
|
-
filters:
|
|
172
|
+
agent_id: agent_id,
|
|
173
|
+
agent_depth: agent_depth,
|
|
174
|
+
permissions: @permissions,
|
|
175
|
+
auth: auth_descriptor,
|
|
176
|
+
tenant_id: tenant_id,
|
|
177
|
+
classes: filter_descriptor(@class_filter_only, @class_filter_except),
|
|
178
|
+
tools: tools_descriptor,
|
|
179
|
+
methods: filter_descriptor(@method_filter_only, @method_filter_except, transform: ->(s) { s.to_s }),
|
|
180
|
+
filters: per_agent_filters_summary,
|
|
181
181
|
hidden_classes: Parse::Agent::MetadataRegistry.hidden_class_names,
|
|
182
|
-
per_class:
|
|
183
|
-
strict_modes:
|
|
184
|
-
tool_filter:
|
|
182
|
+
per_class: per_class_descriptor,
|
|
183
|
+
strict_modes: {
|
|
184
|
+
tool_filter: strict_tool_filter?,
|
|
185
185
|
class_filter: strict_class_filter?,
|
|
186
186
|
},
|
|
187
187
|
correlation_id: @correlation_id,
|
|
188
|
-
prompt:
|
|
188
|
+
prompt: { version: Parse::Agent::PROMPT_VERSION },
|
|
189
189
|
}
|
|
190
190
|
end
|
|
191
191
|
|
|
@@ -230,8 +230,8 @@ module Parse
|
|
|
230
230
|
|
|
231
231
|
def tools_descriptor
|
|
232
232
|
{
|
|
233
|
-
only:
|
|
234
|
-
except:
|
|
233
|
+
only: @tool_filter_only && @tool_filter_only.to_a.sort,
|
|
234
|
+
except: @tool_filter_except && @tool_filter_except.to_a.sort,
|
|
235
235
|
effective: allowed_tools.sort,
|
|
236
236
|
}
|
|
237
237
|
end
|
|
@@ -250,7 +250,7 @@ module Parse
|
|
|
250
250
|
# class.
|
|
251
251
|
def per_class_descriptor
|
|
252
252
|
names = Set.new
|
|
253
|
-
names.merge(@class_filter_only.to_a)
|
|
253
|
+
names.merge(@class_filter_only.to_a) if @class_filter_only
|
|
254
254
|
names.merge(@class_filter_except.to_a) if @class_filter_except
|
|
255
255
|
if @filters
|
|
256
256
|
names.merge(@filters.keys.reject { |k| k == :default }.map(&:to_s))
|
|
@@ -280,7 +280,7 @@ module Parse
|
|
|
280
280
|
def describe_class_accessibility(class_name)
|
|
281
281
|
if Parse::Agent::MetadataRegistry.hidden?(class_name)
|
|
282
282
|
except = Parse::Agent::MetadataRegistry.respond_to?(:hidden_exception_for) ?
|
|
283
|
-
|
|
283
|
+
Parse::Agent::MetadataRegistry.hidden_exception_for(class_name) : nil
|
|
284
284
|
if except == :master_key && auth_context[:using_master_key] == true
|
|
285
285
|
# Hidden from session-bound / acl_user / acl_role agents but
|
|
286
286
|
# reachable by this master-key agent.
|
|
@@ -308,7 +308,7 @@ module Parse
|
|
|
308
308
|
def class_tenant_scope(class_name)
|
|
309
309
|
return nil if tenant_id.nil?
|
|
310
310
|
rule = Parse::Agent::MetadataRegistry.respond_to?(:tenant_scope_rule) ?
|
|
311
|
-
|
|
311
|
+
Parse::Agent::MetadataRegistry.tenant_scope_rule(class_name) : nil
|
|
312
312
|
return nil unless rule
|
|
313
313
|
{ field: rule[:field], value: tenant_id }
|
|
314
314
|
end
|
|
@@ -361,8 +361,8 @@ module Parse
|
|
|
361
361
|
lines = []
|
|
362
362
|
auth = data[:auth]
|
|
363
363
|
auth_line = auth[:mode] == :session_token ?
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
"#{auth[:mode]} (fingerprint=#{auth[:fingerprint]})" :
|
|
365
|
+
auth[:mode].to_s
|
|
366
366
|
lines << "Parse::Agent #{data[:agent_id]} (depth=#{data[:agent_depth]}, correlation=#{data[:correlation_id] || "—"})"
|
|
367
367
|
lines << " auth: #{auth_line}"
|
|
368
368
|
lines << " permissions: #{data[:permissions]}"
|
|
@@ -370,20 +370,20 @@ module Parse
|
|
|
370
370
|
|
|
371
371
|
if data[:classes][:only] || data[:classes][:except]
|
|
372
372
|
lines << " classes:"
|
|
373
|
-
lines << " only: #{data[:classes][:only].inspect}"
|
|
373
|
+
lines << " only: #{data[:classes][:only].inspect}" if data[:classes][:only]
|
|
374
374
|
lines << " except: #{data[:classes][:except].inspect}" if data[:classes][:except]
|
|
375
375
|
else
|
|
376
376
|
lines << " classes: (no filter — every visible class reachable)"
|
|
377
377
|
end
|
|
378
378
|
|
|
379
379
|
lines << " tools:"
|
|
380
|
-
lines << " only: #{data[:tools][:only].inspect}"
|
|
381
|
-
lines << " except: #{data[:tools][:except].inspect}"
|
|
380
|
+
lines << " only: #{data[:tools][:only].inspect}" if data[:tools][:only]
|
|
381
|
+
lines << " except: #{data[:tools][:except].inspect}" if data[:tools][:except]
|
|
382
382
|
lines << " effective: #{data[:tools][:effective].inspect}"
|
|
383
383
|
|
|
384
384
|
if data[:methods][:only] || data[:methods][:except]
|
|
385
385
|
lines << " methods:"
|
|
386
|
-
lines << " only: #{data[:methods][:only].inspect}"
|
|
386
|
+
lines << " only: #{data[:methods][:only].inspect}" if data[:methods][:only]
|
|
387
387
|
lines << " except: #{data[:methods][:except].inspect}" if data[:methods][:except]
|
|
388
388
|
end
|
|
389
389
|
|
data/lib/parse/agent/errors.rb
CHANGED
|
@@ -68,10 +68,10 @@ module Parse
|
|
|
68
68
|
def initialize(class_name = nil, message = nil,
|
|
69
69
|
kind: nil, denied_field: nil, allowed_fields: nil,
|
|
70
70
|
suggested_rewrite: nil)
|
|
71
|
-
@class_name
|
|
72
|
-
@kind
|
|
73
|
-
@denied_field
|
|
74
|
-
@allowed_fields
|
|
71
|
+
@class_name = class_name.to_s
|
|
72
|
+
@kind = kind
|
|
73
|
+
@denied_field = denied_field
|
|
74
|
+
@allowed_fields = allowed_fields&.map(&:to_s)
|
|
75
75
|
@suggested_rewrite = suggested_rewrite
|
|
76
76
|
super(message || "Class '#{@class_name}' is not accessible to this agent")
|
|
77
77
|
end
|
|
@@ -81,9 +81,9 @@ module Parse
|
|
|
81
81
|
# unused nil fields.
|
|
82
82
|
def to_details
|
|
83
83
|
{
|
|
84
|
-
kind:
|
|
85
|
-
denied_field:
|
|
86
|
-
allowed_fields:
|
|
84
|
+
kind: kind,
|
|
85
|
+
denied_field: denied_field,
|
|
86
|
+
allowed_fields: allowed_fields,
|
|
87
87
|
suggested_rewrite: suggested_rewrite,
|
|
88
88
|
}.compact
|
|
89
89
|
end
|
|
@@ -116,8 +116,8 @@ module Parse
|
|
|
116
116
|
def initialize(message = nil, depth: nil)
|
|
117
117
|
@depth = depth
|
|
118
118
|
super(message || "Parse::Agent recursion depth exhausted (depth=#{depth.inspect}). " \
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
"A sub-agent attempted to construct another sub-agent past the " \
|
|
120
|
+
"configured recursion_depth: cap.")
|
|
121
121
|
end
|
|
122
122
|
end
|
|
123
123
|
|