parse-stack-next 4.5.0
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 +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# Note: Do not require "../object" here - this file is loaded from object.rb
|
|
4
|
+
# and adding that require would create a circular dependency.
|
|
5
|
+
# user.rb is also loaded from object.rb before this file.
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
# This class represents the data and columns contained in the standard Parse `_Role` collection.
|
|
9
|
+
# Roles allow the an application to group a set of {Parse::User} records with the same set of
|
|
10
|
+
# permissions, so that specific records in the database can have {Parse::ACL}s related to a role
|
|
11
|
+
# than trying to add all the users in a group.
|
|
12
|
+
#
|
|
13
|
+
# The default schema for {Role} is as follows:
|
|
14
|
+
#
|
|
15
|
+
# class Parse::Role < Parse::Object
|
|
16
|
+
# # See Parse::Object for inherited properties...
|
|
17
|
+
#
|
|
18
|
+
# property :name
|
|
19
|
+
#
|
|
20
|
+
# # A role may have child roles.
|
|
21
|
+
# has_many :roles, through: :relation
|
|
22
|
+
#
|
|
23
|
+
# # The set of users who belong to this role.
|
|
24
|
+
# has_many :users, through: :relation
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @example Creating and managing roles
|
|
28
|
+
# # Create an admin role
|
|
29
|
+
# admin = Parse::Role.create(name: "Admin")
|
|
30
|
+
#
|
|
31
|
+
# # Add users to the role
|
|
32
|
+
# admin.add_user(user1)
|
|
33
|
+
# admin.add_users(user2, user3)
|
|
34
|
+
# admin.save
|
|
35
|
+
#
|
|
36
|
+
# # Create role hierarchy. Parse Server's _Role semantics: when role X
|
|
37
|
+
# # holds role Y in its `roles` relation, USERS OF Y INHERIT X'S
|
|
38
|
+
# # PERMISSIONS — not the other way around. So if you want Admins to
|
|
39
|
+
# # have everything Moderators can do, you must put Admin into the
|
|
40
|
+
# # Moderator role's `roles` relation:
|
|
41
|
+
# moderator = Parse::Role.create(name: "Moderator")
|
|
42
|
+
# moderator.add_child_role(admin) # Admins inherit Moderator permissions
|
|
43
|
+
# moderator.save
|
|
44
|
+
#
|
|
45
|
+
# # Query users in role (including child roles whose users implicitly
|
|
46
|
+
# # have this role through Parse's inheritance):
|
|
47
|
+
# all_users = moderator.all_users # includes Admin's users transitively
|
|
48
|
+
#
|
|
49
|
+
# @see Parse::Object
|
|
50
|
+
class Role < Parse::Object
|
|
51
|
+
parse_class Parse::Model::CLASS_ROLE
|
|
52
|
+
# @!attribute name
|
|
53
|
+
# @return [String] the name of this role.
|
|
54
|
+
property :name
|
|
55
|
+
# This attribute is mapped as a `has_many` Parse relation association with the {Parse::Role} class,
|
|
56
|
+
# as roles can be associated with multiple child roles to support role inheritance.
|
|
57
|
+
# The roles Parse relation provides a mechanism to create a hierarchical inheritable types of permissions
|
|
58
|
+
# by assigning child roles.
|
|
59
|
+
# @return [RelationCollectionProxy<Role>] a collection of Roles.
|
|
60
|
+
has_many :roles, through: :relation
|
|
61
|
+
# This attribute is mapped as a `has_many` Parse relation association with the {Parse::User} class.
|
|
62
|
+
# @return [RelationCollectionProxy<User>] a Parse relation of users belonging to this role.
|
|
63
|
+
has_many :users, through: :relation
|
|
64
|
+
|
|
65
|
+
# Parse Server requires every _Role row to ship with an ACL (the
|
|
66
|
+
# requirement is hard-coded in SchemaController.requiredColumns and
|
|
67
|
+
# cannot be disabled by config). We default to master-only (ACL = {})
|
|
68
|
+
# so anonymous clients cannot enumerate the role graph or read
|
|
69
|
+
# membership. Parse Server's internal role expansion runs with master
|
|
70
|
+
# context (Auth#getRolesForUser), so ACL evaluation continues to work
|
|
71
|
+
# without a public-read grant. Apps that need broader access should
|
|
72
|
+
# pass `acl:` to find_or_create / assign `role.acl=` explicitly.
|
|
73
|
+
acl_policy :private
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
# Find a role by its name.
|
|
77
|
+
# @param role_name [String] the name of the role to find.
|
|
78
|
+
# @return [Parse::Role, nil] the role if found, nil otherwise.
|
|
79
|
+
# @example
|
|
80
|
+
# admin = Parse::Role.find_by_name("Admin")
|
|
81
|
+
def find_by_name(role_name)
|
|
82
|
+
query(name: role_name).first
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Find or create a role by name.
|
|
86
|
+
# @param role_name [String] the name of the role.
|
|
87
|
+
# @param acl [Parse::ACL] optional ACL to set on creation.
|
|
88
|
+
# @return [Parse::Role] the existing or newly created role.
|
|
89
|
+
# @example
|
|
90
|
+
# admin = Parse::Role.find_or_create("Admin")
|
|
91
|
+
def find_or_create(role_name, acl: nil)
|
|
92
|
+
role = find_by_name(role_name)
|
|
93
|
+
return role if role
|
|
94
|
+
|
|
95
|
+
role = new(name: role_name)
|
|
96
|
+
role.acl = acl if acl
|
|
97
|
+
role.save
|
|
98
|
+
role
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get all role names in the system.
|
|
102
|
+
# @return [Array<String>] array of role names.
|
|
103
|
+
def all_names
|
|
104
|
+
query.results.map(&:name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if a role with the given name exists.
|
|
108
|
+
# @param role_name [String] the name to check.
|
|
109
|
+
# @return [Boolean] true if role exists.
|
|
110
|
+
def exists?(role_name)
|
|
111
|
+
query(name: role_name).count > 0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Return the transitive upward closure of role names a user
|
|
115
|
+
# inherits permissions from.
|
|
116
|
+
#
|
|
117
|
+
# Parse Server +_Role+ inheritance: when role +X+ holds role +Y+
|
|
118
|
+
# in its +roles+ relation, users of +Y+ inherit +X+'s
|
|
119
|
+
# permissions. So given a user +U+, the permission set is built
|
|
120
|
+
# by:
|
|
121
|
+
#
|
|
122
|
+
# 1. Querying for every role +D+ where +U+ is a direct member
|
|
123
|
+
# (+_Role.users+ contains +U+).
|
|
124
|
+
# 2. For each direct role +D+, walking upward to every role
|
|
125
|
+
# +P+ that lists +D+ in its +roles+ relation. Repeat until
|
|
126
|
+
# no new parents are found.
|
|
127
|
+
#
|
|
128
|
+
# This is the correct primitive for building +_rperm+ predicates
|
|
129
|
+
# (e.g., {ACLReadableByConstraint}, {ACLWritableByConstraint},
|
|
130
|
+
# and the Atlas Search ACL +$match+ injection). The legacy walk
|
|
131
|
+
# via {#all_child_roles} on the user's direct roles traverses
|
|
132
|
+
# the wrong direction and over-grants — it returns roles whose
|
|
133
|
+
# users include the input user through inheritance, not the
|
|
134
|
+
# roles the input user inherits permissions from.
|
|
135
|
+
#
|
|
136
|
+
# Cycle-safe: a visited-id set guards against pathological
|
|
137
|
+
# +_Role.roles+ cycles (e.g. A→B→A).
|
|
138
|
+
#
|
|
139
|
+
# @param user [Parse::User, Parse::Pointer, String, nil] the
|
|
140
|
+
# user to expand. A +Parse::Pointer+ must be on the +_User+
|
|
141
|
+
# class. A +String+ is treated as a +_User+ objectId. +nil+
|
|
142
|
+
# returns an empty set (anonymous).
|
|
143
|
+
# @param max_depth [Integer] maximum BFS depth (default: 10).
|
|
144
|
+
# @param master [Boolean] when +true+, opt in to the mongo-direct
|
|
145
|
+
# fast path under master-mode (bypasses `_Role` CLP). Use for
|
|
146
|
+
# admin/analytics code paths that legitimately need a
|
|
147
|
+
# master-scope view of the role graph.
|
|
148
|
+
# @param as [Parse::User, Parse::Pointer, nil] when supplied,
|
|
149
|
+
# opt in to the mongo-direct fast path under the caller's
|
|
150
|
+
# scope (subject to `_Role` CLP). The scope is forwarded
|
|
151
|
+
# verbatim to {Parse::MongoDB.role_names_for_user}; CLP denial
|
|
152
|
+
# raises {Parse::CLPScope::Denied}.
|
|
153
|
+
# @return [Set<String>] role names (no +role:+ prefix) the user
|
|
154
|
+
# transitively inherits permissions from, including direct
|
|
155
|
+
# memberships. Empty set for anonymous or no-membership users.
|
|
156
|
+
# @note When neither `master:` nor `as:` is supplied, the
|
|
157
|
+
# mongo-direct fast path is **skipped**; the method falls
|
|
158
|
+
# through to the Parse-Server walk
|
|
159
|
+
# (`Parse::Role.all(users: user_pointer)`) which goes through
|
|
160
|
+
# the default Parse::Client. This preserves backward
|
|
161
|
+
# compatibility for the many SDK-internal call sites that
|
|
162
|
+
# compose ACL scopes (acl_scope, atlas_search session,
|
|
163
|
+
# query/constraints) — none of those have a caller scope to
|
|
164
|
+
# forward. The fast path is opt-in for performance-conscious
|
|
165
|
+
# callers that can supply explicit authorization.
|
|
166
|
+
# @example
|
|
167
|
+
# names = Parse::Role.all_for_user(user, master: true) # admin/analytics
|
|
168
|
+
# names = Parse::Role.all_for_user(user, as: current_user) # scope-checked
|
|
169
|
+
def all_for_user(user, max_depth: 10, master: false, as: nil)
|
|
170
|
+
names = Set.new
|
|
171
|
+
return names if user.nil? || max_depth <= 0
|
|
172
|
+
|
|
173
|
+
user_pointer = role_lookup_pointer_for(user)
|
|
174
|
+
return names if user_pointer.nil?
|
|
175
|
+
|
|
176
|
+
# The fast path is opt-in. When neither `master:` nor `as:` is
|
|
177
|
+
# supplied, skip it entirely — the underlying mongo helper
|
|
178
|
+
# would raise ArgumentError, and we don't want to surprise
|
|
179
|
+
# the many backward-compat call sites (acl_scope.resolve_for_user,
|
|
180
|
+
# atlas_search Session.role_names_for, query/constraints' ACL
|
|
181
|
+
# constraint building, agent default-scope composition) that
|
|
182
|
+
# have no scope to forward.
|
|
183
|
+
if master == true || !as.nil?
|
|
184
|
+
fast_path_result = all_for_user_mongo_fast_path(
|
|
185
|
+
user_pointer.id, max_depth, master: master, as: as,
|
|
186
|
+
)
|
|
187
|
+
if fast_path_result.is_a?(Set)
|
|
188
|
+
ActiveSupport::Notifications.instrument(
|
|
189
|
+
"parse.role.expand",
|
|
190
|
+
direction: :forward, target_id: user_pointer.id,
|
|
191
|
+
depth: max_depth, source: :mongo_direct,
|
|
192
|
+
result_count: fast_path_result.size,
|
|
193
|
+
)
|
|
194
|
+
return fast_path_result
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
begin
|
|
199
|
+
direct_roles = Parse::Role.all(users: user_pointer)
|
|
200
|
+
rescue
|
|
201
|
+
return names
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
result = expand_inheritance_upward(direct_roles, max_depth: max_depth)
|
|
205
|
+
ActiveSupport::Notifications.instrument(
|
|
206
|
+
"parse.role.expand",
|
|
207
|
+
direction: :forward, target_id: user_pointer.id,
|
|
208
|
+
depth: max_depth, source: :parse_server,
|
|
209
|
+
result_count: result.size,
|
|
210
|
+
)
|
|
211
|
+
result
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @!visibility private
|
|
215
|
+
# Try the mongo-direct fast path for {.all_for_user}. Returns the
|
|
216
|
+
# resolved `Set<String>` on success, or `nil` when the fast path
|
|
217
|
+
# is unavailable (mongo not configured, or a benign availability
|
|
218
|
+
# error). Attack-signal errors (timeouts, denied operators,
|
|
219
|
+
# CLP::Denied, ArgumentError on missing auth) are propagated.
|
|
220
|
+
def all_for_user_mongo_fast_path(user_id, max_depth, master: false, as: nil)
|
|
221
|
+
return nil unless defined?(Parse::MongoDB)
|
|
222
|
+
return nil unless Parse::MongoDB.respond_to?(:role_names_for_user)
|
|
223
|
+
Parse::MongoDB.role_names_for_user(
|
|
224
|
+
user_id, max_depth: max_depth, master: master, as: as,
|
|
225
|
+
)
|
|
226
|
+
rescue StandardError => e
|
|
227
|
+
# Fall back to Parse-Server path on benign availability errors
|
|
228
|
+
# (lost connection mid-query). Propagate everything else —
|
|
229
|
+
# ExecutionTimeout, DeniedOperator, CLPScope::Denied,
|
|
230
|
+
# ArgumentError, and any unrecognized Mongo::Error subclass —
|
|
231
|
+
# so attack signals aren't masked by a silent slow-path retry.
|
|
232
|
+
if defined?(::Mongo::Error::ConnectionFailure) &&
|
|
233
|
+
e.is_a?(::Mongo::Error::ConnectionFailure)
|
|
234
|
+
# Emit a structured event so operators can observe the
|
|
235
|
+
# fast-path-unavailable rate (e.g. analytics-replica
|
|
236
|
+
# connection flapping). The fallback to the Parse-Server
|
|
237
|
+
# walk that follows enforces ACL on its own; this notification
|
|
238
|
+
# exists solely to surface the discrepancy.
|
|
239
|
+
ActiveSupport::Notifications.instrument(
|
|
240
|
+
"parse.role.fast_path_unavailable",
|
|
241
|
+
reason: "connection_failure", direction: :forward,
|
|
242
|
+
target_id: user_id, depth: max_depth,
|
|
243
|
+
)
|
|
244
|
+
nil
|
|
245
|
+
else
|
|
246
|
+
raise
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Walk upward from a starting frontier of {Parse::Role} objects
|
|
251
|
+
# through the +_Role.roles+ inverse relation, collecting every
|
|
252
|
+
# role name reachable. Used by {.all_for_user} (frontier = the
|
|
253
|
+
# user's direct roles) and {Parse::Role#all_parent_role_names}
|
|
254
|
+
# (frontier = the role itself).
|
|
255
|
+
#
|
|
256
|
+
# The starting frontier is INCLUDED in the returned set, because
|
|
257
|
+
# the semantics is "every role name whose presence in +_rperm+
|
|
258
|
+
# grants access" — direct membership counts.
|
|
259
|
+
#
|
|
260
|
+
# @param starting_roles [Array<Parse::Role>] roles to begin the
|
|
261
|
+
# upward traversal from.
|
|
262
|
+
# @param max_depth [Integer] maximum BFS depth.
|
|
263
|
+
# @return [Set<String>] role names (no +role:+ prefix) including
|
|
264
|
+
# the starting frontier and every transitive parent.
|
|
265
|
+
def expand_inheritance_upward(starting_roles, max_depth: 10)
|
|
266
|
+
names = Set.new
|
|
267
|
+
visited_ids = Set.new
|
|
268
|
+
frontier = []
|
|
269
|
+
|
|
270
|
+
Array(starting_roles).each do |role|
|
|
271
|
+
next if role.nil? || role.id.nil?
|
|
272
|
+
next if visited_ids.include?(role.id)
|
|
273
|
+
visited_ids << role.id
|
|
274
|
+
names << role.name if role.respond_to?(:name) && role.name.present?
|
|
275
|
+
frontier << role
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
depth = 0
|
|
279
|
+
while frontier.any? && depth < max_depth
|
|
280
|
+
next_frontier = []
|
|
281
|
+
frontier.each do |role|
|
|
282
|
+
next if role.nil? || role.id.nil?
|
|
283
|
+
begin
|
|
284
|
+
parents = Parse::Role.all(roles: role)
|
|
285
|
+
rescue
|
|
286
|
+
next
|
|
287
|
+
end
|
|
288
|
+
parents.each do |parent|
|
|
289
|
+
next if parent.nil? || parent.id.nil?
|
|
290
|
+
next if visited_ids.include?(parent.id)
|
|
291
|
+
visited_ids << parent.id
|
|
292
|
+
names << parent.name if parent.respond_to?(:name) && parent.name.present?
|
|
293
|
+
next_frontier << parent
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
frontier = next_frontier
|
|
297
|
+
depth += 1
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
names
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
# @!visibility private
|
|
306
|
+
# Coerce caller-supplied user argument into a +Parse::Pointer+
|
|
307
|
+
# on +_User+ suitable for an inverse-relation query. Returns
|
|
308
|
+
# +nil+ when the input cannot be resolved to a +_User+ id, in
|
|
309
|
+
# which case {.all_for_user} returns an empty set without
|
|
310
|
+
# issuing a network call.
|
|
311
|
+
def role_lookup_pointer_for(user)
|
|
312
|
+
if user.is_a?(Parse::User)
|
|
313
|
+
return nil unless user.id.present?
|
|
314
|
+
user
|
|
315
|
+
elsif user.is_a?(Parse::Pointer)
|
|
316
|
+
klass = user.parse_class
|
|
317
|
+
return nil unless klass == Parse::Model::CLASS_USER || klass == "User"
|
|
318
|
+
return nil unless user.id.present?
|
|
319
|
+
user
|
|
320
|
+
elsif user.is_a?(String) && !user.strip.empty?
|
|
321
|
+
Parse::Pointer.new(Parse::Model::CLASS_USER, user)
|
|
322
|
+
else
|
|
323
|
+
nil
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Add a single user to this role.
|
|
329
|
+
# @param user [Parse::User] the user to add.
|
|
330
|
+
# @return [self] returns self for chaining.
|
|
331
|
+
# @example
|
|
332
|
+
# role.add_user(user).save
|
|
333
|
+
def add_user(user)
|
|
334
|
+
users.add(user)
|
|
335
|
+
self
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Add multiple users to this role.
|
|
339
|
+
# @param user_list [Array<Parse::User>] users to add.
|
|
340
|
+
# @return [self] returns self for chaining.
|
|
341
|
+
# @example
|
|
342
|
+
# role.add_users(user1, user2, user3).save
|
|
343
|
+
def add_users(*user_list)
|
|
344
|
+
users.add(user_list.flatten)
|
|
345
|
+
self
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Remove a single user from this role.
|
|
349
|
+
# @param user [Parse::User] the user to remove.
|
|
350
|
+
# @return [self] returns self for chaining.
|
|
351
|
+
def remove_user(user)
|
|
352
|
+
users.remove(user)
|
|
353
|
+
self
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Remove multiple users from this role.
|
|
357
|
+
# @param user_list [Array<Parse::User>] users to remove.
|
|
358
|
+
# @return [self] returns self for chaining.
|
|
359
|
+
def remove_users(*user_list)
|
|
360
|
+
users.remove(user_list.flatten)
|
|
361
|
+
self
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Add a child role to this role's hierarchy.
|
|
365
|
+
#
|
|
366
|
+
# **The method name is misleading — prefer {#grant_capabilities_to!}
|
|
367
|
+
# or {#inherits_capabilities_from!}.** `add_child_role` mutates the
|
|
368
|
+
# receiver's `roles` relation; per Parse Server semantics, putting
|
|
369
|
+
# role Y in role X's `roles` relation grants X's capabilities to
|
|
370
|
+
# users-of-Y. The "child" terminology has the inheritance direction
|
|
371
|
+
# exactly inverted from intuitive org-chart reading. Retained for
|
|
372
|
+
# backward compatibility and as the low-level structural primitive;
|
|
373
|
+
# new callers should use the direction-explicit semantic methods.
|
|
374
|
+
#
|
|
375
|
+
# IMPORTANT — Parse Server _Role inheritance semantics: when role X
|
|
376
|
+
# holds role Y in its `roles` relation, **users of Y inherit X's
|
|
377
|
+
# permissions** (not the other way around). So calling
|
|
378
|
+
# +admin.add_child_role(moderator)+ does NOT grant Moderator's
|
|
379
|
+
# capabilities to Admin; it grants Admin's capabilities to every
|
|
380
|
+
# Moderator user — privilege escalation.
|
|
381
|
+
#
|
|
382
|
+
# If you want Admins to have everything Moderators can do, you need
|
|
383
|
+
# to add ADMIN to MODERATOR's roles relation:
|
|
384
|
+
#
|
|
385
|
+
# moderator.add_child_role(admin) # Admins now have Moderator capabilities
|
|
386
|
+
#
|
|
387
|
+
# Direction-explicit replacements:
|
|
388
|
+
#
|
|
389
|
+
# admin.inherits_capabilities_from!(moderator) # admin perspective
|
|
390
|
+
# moderator.grant_capabilities_to!(admin) # moderator perspective
|
|
391
|
+
#
|
|
392
|
+
# Both bang variants auto-save and return self.
|
|
393
|
+
#
|
|
394
|
+
# @param role [Parse::Role] the role to add to this role's `roles` relation.
|
|
395
|
+
# @return [self] returns self for chaining.
|
|
396
|
+
# @raise [ArgumentError] when +role+ is +self+ (a self-loop in the `_Role.roles` relation produces an infinite recursion on lookup and serves no permission purpose).
|
|
397
|
+
def add_child_role(role)
|
|
398
|
+
assert_not_self_reference!(role, :add_child_role)
|
|
399
|
+
roles.add(role)
|
|
400
|
+
self
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Add multiple child roles to this role's hierarchy. See
|
|
404
|
+
# {#add_child_role} for the inheritance-direction caveat.
|
|
405
|
+
# @param role_list [Array<Parse::Role>] roles to add.
|
|
406
|
+
# @return [self] returns self for chaining.
|
|
407
|
+
# @raise [ArgumentError] when any entry in +role_list+ is +self+.
|
|
408
|
+
def add_child_roles(*role_list)
|
|
409
|
+
flat = role_list.flatten
|
|
410
|
+
flat.each { |r| assert_not_self_reference!(r, :add_child_roles) }
|
|
411
|
+
roles.add(flat)
|
|
412
|
+
self
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Remove a child role from this role's hierarchy.
|
|
416
|
+
# @param role [Parse::Role] the child role to remove.
|
|
417
|
+
# @return [self] returns self for chaining.
|
|
418
|
+
def remove_child_role(role)
|
|
419
|
+
roles.remove(role)
|
|
420
|
+
self
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Remove multiple child roles from this role's hierarchy.
|
|
424
|
+
# @param role_list [Array<Parse::Role>] child roles to remove.
|
|
425
|
+
# @return [self] returns self for chaining.
|
|
426
|
+
def remove_child_roles(*role_list)
|
|
427
|
+
roles.remove(role_list.flatten)
|
|
428
|
+
self
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Grant this role's capabilities to the given role's users. Reads as:
|
|
432
|
+
# "users with +grantee+ now have +self+'s capabilities."
|
|
433
|
+
# Equivalent to +self.add_child_role(grantee)+ but unambiguous about
|
|
434
|
+
# the direction of inheritance.
|
|
435
|
+
#
|
|
436
|
+
# Non-saving — the caller must call +self.save+ to persist. See
|
|
437
|
+
# {#grant_capabilities_to!} for the auto-saving variant.
|
|
438
|
+
#
|
|
439
|
+
# @param grantee [Parse::Role] the role whose users will inherit this role's permissions.
|
|
440
|
+
# @return [self] returns self for chaining.
|
|
441
|
+
# @example
|
|
442
|
+
# moderator.grant_capabilities_to(admin).save
|
|
443
|
+
# # → Admin users can now do anything Moderator users can
|
|
444
|
+
def grant_capabilities_to(grantee)
|
|
445
|
+
assert_not_self_reference!(grantee, :grant_capabilities_to)
|
|
446
|
+
roles.add(grantee)
|
|
447
|
+
self
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Auto-saving variant of {#grant_capabilities_to}. Performs the
|
|
451
|
+
# relation mutation AND persists +self+ in one call. Returns +self+
|
|
452
|
+
# consistently so the caller can chain or store the result without
|
|
453
|
+
# tracking which object was mutated. Prefer this in tests and
|
|
454
|
+
# one-shot scripts where batching multiple mutations isn't needed.
|
|
455
|
+
#
|
|
456
|
+
# @param grantee [Parse::Role] the role whose users will inherit this role's permissions.
|
|
457
|
+
# @return [self] the mutated and persisted self.
|
|
458
|
+
# @raise [Parse::RecordNotSaved] if the save fails.
|
|
459
|
+
# @example
|
|
460
|
+
# moderator.grant_capabilities_to!(admin)
|
|
461
|
+
# # → Admin users can now do anything Moderator users can. Persisted.
|
|
462
|
+
def grant_capabilities_to!(grantee)
|
|
463
|
+
grant_capabilities_to(grantee)
|
|
464
|
+
save!
|
|
465
|
+
self
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Inverse spelling of {#grant_capabilities_to}: "this role's users
|
|
469
|
+
# inherit +source+'s capabilities". Performs the relation mutation
|
|
470
|
+
# on +source+, not on +self+.
|
|
471
|
+
#
|
|
472
|
+
# **Save target.** The mutation lives on +source.roles+. To persist,
|
|
473
|
+
# the caller must save +source+, NOT +self+. This asymmetry exists
|
|
474
|
+
# because Parse Server stores the relation on the role that holds
|
|
475
|
+
# the +roles+ list, and that role is +source+. The non-bang form is
|
|
476
|
+
# retained for callers that need to batch multiple mutations on
|
|
477
|
+
# +source+ before a single save; prefer {#inherits_capabilities_from!}
|
|
478
|
+
# for the one-shot case where the auto-save matches intent.
|
|
479
|
+
#
|
|
480
|
+
# @param source [Parse::Role] the role whose capabilities this role's users acquire.
|
|
481
|
+
# @return [Parse::Role] the +source+ role (caller still needs to .save it
|
|
482
|
+
# if not using the bang variant).
|
|
483
|
+
# @example Non-saving (must save source separately)
|
|
484
|
+
# admin.inherits_capabilities_from(moderator)
|
|
485
|
+
# moderator.save
|
|
486
|
+
# @example Auto-saving via the bang variant
|
|
487
|
+
# admin.inherits_capabilities_from!(moderator)
|
|
488
|
+
# # → Admin users can now do anything Moderator users can. Persisted.
|
|
489
|
+
def inherits_capabilities_from(source)
|
|
490
|
+
assert_not_self_reference!(source, :inherits_capabilities_from)
|
|
491
|
+
source.roles.add(self)
|
|
492
|
+
source
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Auto-saving variant of {#inherits_capabilities_from}. Performs the
|
|
496
|
+
# mutation on +source.roles+ AND saves +source+ for you, then
|
|
497
|
+
# returns +self+ so the caller can keep working with the role they
|
|
498
|
+
# called the method on. Resolves the most common stumbling block
|
|
499
|
+
# with {#inherits_capabilities_from}: the "save target" asymmetry.
|
|
500
|
+
#
|
|
501
|
+
# @param source [Parse::Role] the role whose capabilities this role's users acquire.
|
|
502
|
+
# @return [self] the role that now inherits (caller's original receiver).
|
|
503
|
+
# @raise [Parse::RecordNotSaved] if the save of +source+ fails.
|
|
504
|
+
# @example
|
|
505
|
+
# admin.inherits_capabilities_from!(moderator)
|
|
506
|
+
# # → Admin users can now do anything Moderator users can. Persisted.
|
|
507
|
+
def inherits_capabilities_from!(source)
|
|
508
|
+
inherits_capabilities_from(source)
|
|
509
|
+
source.save!
|
|
510
|
+
self
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Check if a user belongs to this role (direct membership only).
|
|
514
|
+
# @param user [Parse::User] the user to check.
|
|
515
|
+
# @return [Boolean] true if user is a direct member.
|
|
516
|
+
def has_user?(user)
|
|
517
|
+
return false unless user.is_a?(Parse::User) && user.id.present?
|
|
518
|
+
users.query.where(objectId: user.id).count > 0
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Check if a role is a direct child of this role.
|
|
522
|
+
# @param role [Parse::Role] the role to check.
|
|
523
|
+
# @return [Boolean] true if role is a direct child.
|
|
524
|
+
def has_child_role?(role)
|
|
525
|
+
return false unless role.is_a?(Parse::Role) && role.id.present?
|
|
526
|
+
roles.query.where(objectId: role.id).count > 0
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Get all users belonging to this role, including users from child roles recursively.
|
|
530
|
+
#
|
|
531
|
+
# Cycle-safe: a +visited+ set guards against pathological
|
|
532
|
+
# +_Role.roles+ cycles (e.g. A→B→A) that would otherwise cause
|
|
533
|
+
# exponential per-node query fan-out.
|
|
534
|
+
#
|
|
535
|
+
# @param max_depth [Integer] maximum recursion depth.
|
|
536
|
+
# @param visited [Set] internal cycle-detection accumulator.
|
|
537
|
+
# @param master [Boolean] when +true+, opt in to the mongo-direct
|
|
538
|
+
# fast path under master-mode. The follow-up `_User` fetch also
|
|
539
|
+
# runs unscoped — used for admin/analytics paths that need a
|
|
540
|
+
# master-key view of every member.
|
|
541
|
+
# @param as [Parse::User, Parse::Pointer, nil] when supplied, opt
|
|
542
|
+
# in to the mongo-direct fast path under the caller's scope. The
|
|
543
|
+
# `_Role` CLP is checked on entry, the `_User` `_rperm` allow-set
|
|
544
|
+
# is folded into the join sub-pipeline (so the fast path returns
|
|
545
|
+
# only members the scope is allowed to read), and the follow-up
|
|
546
|
+
# `Parse::MongoDB.aggregate` call to hydrate the user rows runs
|
|
547
|
+
# under the same scope (so `_User` ACL fires for both the join
|
|
548
|
+
# filter AND the post-fetch hydration).
|
|
549
|
+
# @return [Array<Parse::User>] all users in the role hierarchy.
|
|
550
|
+
# @note When neither `master:` nor `as:` is supplied, the
|
|
551
|
+
# mongo-direct fast path is skipped; the method falls through
|
|
552
|
+
# to the Parse-Server walk through the per-relation query
|
|
553
|
+
# interface, which goes through the default Parse::Client.
|
|
554
|
+
# @example
|
|
555
|
+
# all_users = admin_role.all_users(master: true)
|
|
556
|
+
# visible = admin_role.all_users(as: current_user)
|
|
557
|
+
def all_users(max_depth: 10, visited: Set.new, master: false, as: nil)
|
|
558
|
+
return [] if max_depth <= 0
|
|
559
|
+
return [] if id.nil? || visited.include?(id)
|
|
560
|
+
|
|
561
|
+
# The fast path is opt-in (same rationale as {.all_for_user}).
|
|
562
|
+
if master == true || !as.nil?
|
|
563
|
+
fast_path = all_users_mongo_fast_path(max_depth, master: master, as: as)
|
|
564
|
+
if fast_path.is_a?(Array)
|
|
565
|
+
ActiveSupport::Notifications.instrument(
|
|
566
|
+
"parse.role.expand",
|
|
567
|
+
direction: :reverse, target_id: id, depth: max_depth,
|
|
568
|
+
source: :mongo_direct, result_count: fast_path.size,
|
|
569
|
+
)
|
|
570
|
+
return fast_path
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
visited << id
|
|
575
|
+
|
|
576
|
+
direct_users = users.all
|
|
577
|
+
|
|
578
|
+
child_roles = roles.all
|
|
579
|
+
child_users = child_roles.flat_map do |child_role|
|
|
580
|
+
child_role.all_users(max_depth: max_depth - 1, visited: visited)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
result = (direct_users + child_users).uniq { |u| u.id }
|
|
584
|
+
ActiveSupport::Notifications.instrument(
|
|
585
|
+
"parse.role.expand",
|
|
586
|
+
direction: :reverse, target_id: id, depth: max_depth,
|
|
587
|
+
source: :parse_server, result_count: result.size,
|
|
588
|
+
)
|
|
589
|
+
result
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# @!visibility private
|
|
593
|
+
# Try the mongo-direct fast path for {#all_users}. Returns an Array
|
|
594
|
+
# of hydrated {Parse::User} objects on success, or `nil` when the
|
|
595
|
+
# fast path is unavailable. Attack-signal errors propagate.
|
|
596
|
+
#
|
|
597
|
+
# When `master: true`, the `_User` follow-up fetch runs through
|
|
598
|
+
# `Parse::User.all` (default client, master-key). When `as: <user>`
|
|
599
|
+
# is supplied, the follow-up runs through `Parse::MongoDB.aggregate`
|
|
600
|
+
# with the caller scope so `_User` ACL fires both server-side
|
|
601
|
+
# (sub-pipeline `_rperm` match in the role-subtree join, see
|
|
602
|
+
# MONGO-4) AND on the hydration query (full _User row-level ACL
|
|
603
|
+
# filtering before the rows hit the wire).
|
|
604
|
+
def all_users_mongo_fast_path(max_depth, master: false, as: nil)
|
|
605
|
+
return nil unless defined?(Parse::MongoDB)
|
|
606
|
+
return nil unless Parse::MongoDB.respond_to?(:users_in_role_subtree)
|
|
607
|
+
ids = Parse::MongoDB.users_in_role_subtree(
|
|
608
|
+
id, max_depth: max_depth, master: master, as: as,
|
|
609
|
+
)
|
|
610
|
+
return nil if ids.nil?
|
|
611
|
+
return [] if ids.empty?
|
|
612
|
+
|
|
613
|
+
if master == true
|
|
614
|
+
# Master path: master-keyed default client returns every row.
|
|
615
|
+
Parse::User.all(:objectId.in => ids.to_a)
|
|
616
|
+
else
|
|
617
|
+
# Scoped path: route through Parse::MongoDB.aggregate so _User
|
|
618
|
+
# ACL is enforced by the SDK on the hydration query. The
|
|
619
|
+
# aggregate already strips protectedFields and filters by
|
|
620
|
+
# _rperm/CLP under the resolved scope.
|
|
621
|
+
hydrate_users_under_scope(ids.to_a, as)
|
|
622
|
+
end
|
|
623
|
+
rescue StandardError => e
|
|
624
|
+
if defined?(::Mongo::Error::ConnectionFailure) &&
|
|
625
|
+
e.is_a?(::Mongo::Error::ConnectionFailure)
|
|
626
|
+
# Emit a structured event so operators can monitor fast-path
|
|
627
|
+
# availability separate from the role-graph notification.
|
|
628
|
+
ActiveSupport::Notifications.instrument(
|
|
629
|
+
"parse.role.fast_path_unavailable",
|
|
630
|
+
reason: "connection_failure", direction: :reverse,
|
|
631
|
+
target_id: id, depth: max_depth,
|
|
632
|
+
)
|
|
633
|
+
nil
|
|
634
|
+
else
|
|
635
|
+
raise
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# @!visibility private
|
|
640
|
+
# Hydrate a list of `_User.objectId`s into {Parse::User} instances
|
|
641
|
+
# via `Parse::MongoDB.aggregate` under the supplied scope. This is
|
|
642
|
+
# the scoped-path hydration that goes through the SDK's ACL
|
|
643
|
+
# enforcement (top-level _rperm match, CLP, protectedFields strip)
|
|
644
|
+
# instead of the master-keyed `Parse::User.all`.
|
|
645
|
+
#
|
|
646
|
+
# Returns an Array of {Parse::User} instances (possibly empty).
|
|
647
|
+
def hydrate_users_under_scope(ids, as_scope)
|
|
648
|
+
return [] if ids.nil? || ids.empty?
|
|
649
|
+
pipeline = [
|
|
650
|
+
{ "$match" => { "_id" => { "$in" => ids.map(&:to_s) } } },
|
|
651
|
+
]
|
|
652
|
+
raw = Parse::MongoDB.aggregate(
|
|
653
|
+
Parse::Model::CLASS_USER, pipeline,
|
|
654
|
+
allow_internal_fields: true, acl_user: as_scope,
|
|
655
|
+
)
|
|
656
|
+
raw.map do |doc|
|
|
657
|
+
parse_doc = Parse::MongoDB.convert_document_to_parse(
|
|
658
|
+
doc, Parse::Model::CLASS_USER,
|
|
659
|
+
)
|
|
660
|
+
Parse::User.new(parse_doc) if parse_doc
|
|
661
|
+
end.compact
|
|
662
|
+
end
|
|
663
|
+
private :hydrate_users_under_scope
|
|
664
|
+
|
|
665
|
+
# Get the set of role names whose presence in a +_rperm+ array
|
|
666
|
+
# grants access to this role's members. That's the role itself
|
|
667
|
+
# plus every role +P+ that lists this role in its +roles+ relation,
|
|
668
|
+
# transitively upward — because users of this role inherit +P+'s
|
|
669
|
+
# permissions under Parse Server's role-inheritance semantics
|
|
670
|
+
# (see {#add_child_role}).
|
|
671
|
+
#
|
|
672
|
+
# The instance-side analogue to {Parse::Role.all_for_user}; the
|
|
673
|
+
# two share an internal BFS via
|
|
674
|
+
# {Parse::Role.expand_inheritance_upward}. Use this method when
|
|
675
|
+
# compiling an ACL predicate around a role argument, e.g.
|
|
676
|
+
# +:ACL.readable_by => admin_role+: the role itself contributes
|
|
677
|
+
# +"role:Admin"+, and any role whose +.roles+ relation contains
|
|
678
|
+
# +admin_role+ also grants Admins access through inheritance.
|
|
679
|
+
#
|
|
680
|
+
# The legacy {#all_child_roles} walk is NOT a substitute. Child
|
|
681
|
+
# roles inherit FROM this role (their members get this role's
|
|
682
|
+
# capabilities), so child-role names in +_rperm+ would not grant
|
|
683
|
+
# this role's members anything — the walk traverses the wrong
|
|
684
|
+
# direction for ACL composition.
|
|
685
|
+
#
|
|
686
|
+
# @param max_depth [Integer] maximum BFS depth (default: 10).
|
|
687
|
+
# @return [Set<String>] role names (no +role:+ prefix) including
|
|
688
|
+
# +self.name+ and every transitive parent.
|
|
689
|
+
# @example
|
|
690
|
+
# permission_strings = admin.all_parent_role_names.map { |n| "role:#{n}" }
|
|
691
|
+
def all_parent_role_names(max_depth: 10)
|
|
692
|
+
Parse::Role.expand_inheritance_upward([self], max_depth: max_depth)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Get all child roles recursively. Cycle-safe; see {#all_users}.
|
|
696
|
+
# @param max_depth [Integer] maximum recursion depth.
|
|
697
|
+
# @param visited [Set] internal cycle-detection accumulator.
|
|
698
|
+
# @return [Array<Parse::Role>] all child roles in the hierarchy.
|
|
699
|
+
def all_child_roles(max_depth: 10, visited: Set.new)
|
|
700
|
+
return [] if max_depth <= 0
|
|
701
|
+
return [] if id.nil? || visited.include?(id)
|
|
702
|
+
visited << id
|
|
703
|
+
|
|
704
|
+
direct_children = roles.all
|
|
705
|
+
nested_children = direct_children.flat_map do |child|
|
|
706
|
+
child.all_child_roles(max_depth: max_depth - 1, visited: visited)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
(direct_children + nested_children).uniq { |r| r.id }
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Get the count of direct users in this role.
|
|
713
|
+
# @return [Integer] number of direct users.
|
|
714
|
+
def users_count
|
|
715
|
+
users.query.count
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Get the count of direct child roles.
|
|
719
|
+
# @return [Integer] number of direct child roles.
|
|
720
|
+
def child_roles_count
|
|
721
|
+
roles.query.count
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# Get the total count of users including child roles.
|
|
725
|
+
# @return [Integer] total user count in hierarchy.
|
|
726
|
+
def total_users_count
|
|
727
|
+
all_users.count
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
private
|
|
731
|
+
|
|
732
|
+
# @!visibility private
|
|
733
|
+
# Refuses a `_Role.roles` mutation that would point a role at itself.
|
|
734
|
+
# The visited-Set guard in {#all_users} / {#all_child_roles} prevents
|
|
735
|
+
# the recursion blowup at read time, but a persisted self-loop still
|
|
736
|
+
# wastes one round-trip per traversal and has no permission effect
|
|
737
|
+
# under Parse Server's role-expansion rules. Reject at write time.
|
|
738
|
+
def assert_not_self_reference!(role, method_name)
|
|
739
|
+
raise ArgumentError,
|
|
740
|
+
"#{method_name} requires a Parse::Role argument (got #{role.class})" unless role.is_a?(Parse::Role)
|
|
741
|
+
same_instance = role.equal?(self)
|
|
742
|
+
same_id = id.present? && role.id.present? && role.id == id
|
|
743
|
+
if same_instance || same_id
|
|
744
|
+
raise ArgumentError,
|
|
745
|
+
"#{method_name} cannot point a role at itself " \
|
|
746
|
+
"(role #{name.inspect}/#{id.inspect}); self-loops in the " \
|
|
747
|
+
"_Role.roles relation serve no permission purpose."
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
end
|