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,544 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
# Class-Level Permissions (CLP) for Parse Server classes.
|
|
6
|
+
#
|
|
7
|
+
# CLPs control access to a class at the schema level, determining who can
|
|
8
|
+
# perform operations on the class and which fields are visible to different
|
|
9
|
+
# users/roles.
|
|
10
|
+
#
|
|
11
|
+
# ## Protected Fields Behavior
|
|
12
|
+
#
|
|
13
|
+
# When a user matches multiple patterns (e.g., public "*", "authenticated", and a role),
|
|
14
|
+
# the protected fields are the **intersection** of all matching patterns. This means
|
|
15
|
+
# a field is only hidden if it's protected by ALL patterns that apply to the user.
|
|
16
|
+
#
|
|
17
|
+
# For example:
|
|
18
|
+
# - `*` protects ["owner", "test"]
|
|
19
|
+
# - `role:Admin` protects ["owner"]
|
|
20
|
+
# - A user with Admin role matches both patterns
|
|
21
|
+
# - Result: only "owner" is hidden (intersection), "test" is visible
|
|
22
|
+
#
|
|
23
|
+
# An empty array `[]` for a pattern means "no fields protected" (user sees everything).
|
|
24
|
+
# If any matching pattern has an empty array, the intersection will also be empty.
|
|
25
|
+
#
|
|
26
|
+
# @example Defining CLPs in a model
|
|
27
|
+
# class Song < Parse::Object
|
|
28
|
+
# property :title, :string
|
|
29
|
+
# property :artist, :string
|
|
30
|
+
# property :internal_notes, :string # Should be hidden from regular users
|
|
31
|
+
#
|
|
32
|
+
# # Set class-level permissions
|
|
33
|
+
# set_clp :find, public: true
|
|
34
|
+
# set_clp :get, public: true
|
|
35
|
+
# set_clp :create, public: false, roles: ["Admin", "Editor"]
|
|
36
|
+
# set_clp :update, public: false, roles: ["Admin", "Editor"]
|
|
37
|
+
# set_clp :delete, public: false, roles: ["Admin"]
|
|
38
|
+
#
|
|
39
|
+
# # Protect fields from certain users
|
|
40
|
+
# protect_fields "*", [:internal_notes, :secret_data] # Hidden from everyone
|
|
41
|
+
# protect_fields "role:Admin", [] # Admins can see everything
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @example Using userField for owner-based access
|
|
45
|
+
# class Document < Parse::Object
|
|
46
|
+
# property :content, :string
|
|
47
|
+
# property :secret, :string
|
|
48
|
+
# belongs_to :owner, as: :user
|
|
49
|
+
#
|
|
50
|
+
# # Hide secret from everyone
|
|
51
|
+
# protect_fields "*", [:secret, :owner]
|
|
52
|
+
# # But owners can see their own document's secret
|
|
53
|
+
# protect_fields "userField:owner", []
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# @example Fetching CLPs from server
|
|
57
|
+
# clp = Song.fetch_clp
|
|
58
|
+
# clp.find_allowed?("role:Admin") # => true
|
|
59
|
+
# clp.protected_fields_for("*") # => ["internal_notes", "secret_data"]
|
|
60
|
+
#
|
|
61
|
+
# @see https://docs.parseplatform.org/rest/guide/#class-level-permissions
|
|
62
|
+
class CLP
|
|
63
|
+
# Valid CLP operation keys for permission-based access
|
|
64
|
+
OPERATIONS = %i[find get count create update delete addField].freeze
|
|
65
|
+
|
|
66
|
+
# Pointer-permission keys (users in these fields get read/write access)
|
|
67
|
+
POINTER_PERMISSIONS = %i[readUserFields writeUserFields].freeze
|
|
68
|
+
|
|
69
|
+
# All valid CLP keys
|
|
70
|
+
ALL_KEYS = (OPERATIONS + POINTER_PERMISSIONS + [:protectedFields]).freeze
|
|
71
|
+
|
|
72
|
+
# @return [Hash] the raw CLP hash
|
|
73
|
+
attr_reader :permissions
|
|
74
|
+
|
|
75
|
+
# Create a new CLP instance.
|
|
76
|
+
# @param data [Hash] optional initial CLP data from Parse Server
|
|
77
|
+
def initialize(data = nil)
|
|
78
|
+
@permissions = {}
|
|
79
|
+
@protected_fields = {}
|
|
80
|
+
parse_data(data) if data.is_a?(Hash)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Parse CLP data from Parse Server format.
|
|
84
|
+
# @param data [Hash] CLP hash from server
|
|
85
|
+
def parse_data(data)
|
|
86
|
+
data.each do |key, value|
|
|
87
|
+
key_sym = key.to_sym
|
|
88
|
+
if key_sym == :protectedFields
|
|
89
|
+
@protected_fields = value.transform_keys(&:to_s)
|
|
90
|
+
elsif OPERATIONS.include?(key_sym)
|
|
91
|
+
@permissions[key_sym] = value.transform_keys(&:to_s)
|
|
92
|
+
elsif POINTER_PERMISSIONS.include?(key_sym)
|
|
93
|
+
# readUserFields and writeUserFields are arrays of field names
|
|
94
|
+
@permissions[key_sym] = Array(value)
|
|
95
|
+
else
|
|
96
|
+
# Store any other keys
|
|
97
|
+
@permissions[key_sym] = value
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Set pointer-permission fields for read access.
|
|
103
|
+
# Users pointed to by these fields can read the object.
|
|
104
|
+
# @param fields [Array<String, Symbol>] pointer field names
|
|
105
|
+
# @return [self]
|
|
106
|
+
# @example
|
|
107
|
+
# clp.set_read_user_fields(:owner, :collaborators)
|
|
108
|
+
def set_read_user_fields(*fields)
|
|
109
|
+
@permissions[:readUserFields] = fields.flatten.map(&:to_s)
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Set pointer-permission fields for write access.
|
|
114
|
+
# Users pointed to by these fields can write to the object.
|
|
115
|
+
# @param fields [Array<String, Symbol>] pointer field names
|
|
116
|
+
# @return [self]
|
|
117
|
+
# @example
|
|
118
|
+
# clp.set_write_user_fields(:owner)
|
|
119
|
+
def set_write_user_fields(*fields)
|
|
120
|
+
@permissions[:writeUserFields] = fields.flatten.map(&:to_s)
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get the read user fields.
|
|
125
|
+
# @return [Array<String>] pointer field names for read access
|
|
126
|
+
def read_user_fields
|
|
127
|
+
@permissions[:readUserFields] || []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get the write user fields.
|
|
131
|
+
# @return [Array<String>] pointer field names for write access
|
|
132
|
+
def write_user_fields
|
|
133
|
+
@permissions[:writeUserFields] || []
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Set permissions for a specific operation.
|
|
137
|
+
# @param operation [Symbol] one of :find, :get, :count, :create, :update, :delete, :addField
|
|
138
|
+
# @param public_access [Boolean, nil] whether public access is allowed
|
|
139
|
+
# @param roles [Array<String>] role names that have access
|
|
140
|
+
# @param users [Array<String>] user objectIds that have access
|
|
141
|
+
# @param pointer_fields [Array<String>] pointer field names for userField access
|
|
142
|
+
# @param requires_authentication [Boolean] whether authentication is required
|
|
143
|
+
# @return [self]
|
|
144
|
+
def set_permission(operation, public_access: nil, roles: [], users: [], pointer_fields: [], requires_authentication: false)
|
|
145
|
+
operation = operation.to_sym
|
|
146
|
+
raise ArgumentError, "Invalid operation: #{operation}" unless OPERATIONS.include?(operation)
|
|
147
|
+
|
|
148
|
+
perm = {}
|
|
149
|
+
|
|
150
|
+
# Handle public access
|
|
151
|
+
# Note: Parse Server only accepts 'true' values for CLP permissions.
|
|
152
|
+
# Setting public: false means "don't grant public access" which is
|
|
153
|
+
# achieved by simply not including the "*" key (absence = no access).
|
|
154
|
+
perm["*"] = true if public_access == true
|
|
155
|
+
|
|
156
|
+
# Handle requiresAuthentication
|
|
157
|
+
perm["requiresAuthentication"] = true if requires_authentication
|
|
158
|
+
|
|
159
|
+
# Handle roles
|
|
160
|
+
Array(roles).each do |role|
|
|
161
|
+
role_key = role.start_with?("role:") ? role : "role:#{role}"
|
|
162
|
+
perm[role_key] = true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Handle users
|
|
166
|
+
Array(users).each do |user_id|
|
|
167
|
+
perm[user_id] = true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Handle pointer fields (userField:fieldName pattern)
|
|
171
|
+
Array(pointer_fields).each do |field|
|
|
172
|
+
field_key = field.start_with?("pointerFields") ? field : "pointerFields"
|
|
173
|
+
perm[field_key] ||= []
|
|
174
|
+
perm[field_key] << field unless field.start_with?("pointerFields")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
@permissions[operation] = perm
|
|
178
|
+
self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Set protected fields for a specific user/role pattern.
|
|
182
|
+
# @param pattern [String] the pattern ("*", "role:RoleName", "userField:fieldName", or user objectId)
|
|
183
|
+
# @param fields [Array<String, Symbol>] field names to protect (hide) from this pattern
|
|
184
|
+
# @return [self]
|
|
185
|
+
# @example
|
|
186
|
+
# clp.set_protected_fields("*", [:email, :phone]) # Hide from everyone
|
|
187
|
+
# clp.set_protected_fields("role:Admin", []) # Admins see everything
|
|
188
|
+
# clp.set_protected_fields("userField:owner", []) # Owners see everything
|
|
189
|
+
def set_protected_fields(pattern, fields)
|
|
190
|
+
pattern = "*" if pattern.to_sym == :public rescue pattern
|
|
191
|
+
@protected_fields[pattern.to_s] = Array(fields).map(&:to_s)
|
|
192
|
+
self
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Get protected fields for a specific pattern.
|
|
196
|
+
# @param pattern [String] the pattern to look up
|
|
197
|
+
# @return [Array<String>] the protected field names
|
|
198
|
+
def protected_fields_for(pattern)
|
|
199
|
+
@protected_fields[pattern.to_s] || []
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Get all protected fields configuration.
|
|
203
|
+
# @return [Hash] pattern => [fields] mapping (deep copy)
|
|
204
|
+
def protected_fields
|
|
205
|
+
@protected_fields.transform_values(&:dup)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Check if a specific pattern has access to an operation.
|
|
209
|
+
# @param operation [Symbol] the operation to check
|
|
210
|
+
# @param pattern [String] the pattern ("*", "role:RoleName", user objectId)
|
|
211
|
+
# @return [Boolean]
|
|
212
|
+
def allowed?(operation, pattern)
|
|
213
|
+
perm = @permissions[operation.to_sym]
|
|
214
|
+
return false unless perm
|
|
215
|
+
|
|
216
|
+
# Check direct access
|
|
217
|
+
return true if perm[pattern.to_s] == true
|
|
218
|
+
return true if perm["*"] == true
|
|
219
|
+
|
|
220
|
+
false
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Check if public access is allowed for an operation.
|
|
224
|
+
# @param operation [Symbol] the operation to check
|
|
225
|
+
# @return [Boolean]
|
|
226
|
+
def public_access?(operation)
|
|
227
|
+
allowed?(operation, "*")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Check if a role has access to an operation.
|
|
231
|
+
# @param operation [Symbol] the operation to check
|
|
232
|
+
# @param role_name [String] the role name (with or without "role:" prefix)
|
|
233
|
+
# @return [Boolean]
|
|
234
|
+
def role_allowed?(operation, role_name)
|
|
235
|
+
role_key = role_name.start_with?("role:") ? role_name : "role:#{role_name}"
|
|
236
|
+
allowed?(operation, role_key)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Convenience methods for checking specific operations
|
|
240
|
+
%i[find get count create update delete addField].each do |op|
|
|
241
|
+
define_method(:"#{op}_allowed?") do |pattern = "*"|
|
|
242
|
+
allowed?(op, pattern)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Check if authentication is required for an operation.
|
|
247
|
+
# @param operation [Symbol] the operation to check
|
|
248
|
+
# @return [Boolean]
|
|
249
|
+
def requires_authentication?(operation)
|
|
250
|
+
perm = @permissions[operation.to_sym]
|
|
251
|
+
return false unless perm
|
|
252
|
+
perm["requiresAuthentication"] == true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Filter fields from a hash based on protected fields for a user/role.
|
|
256
|
+
# This is the core method for filtering webhook responses.
|
|
257
|
+
#
|
|
258
|
+
# Uses **intersection** logic: when a user matches multiple patterns,
|
|
259
|
+
# only fields that are protected by ALL matching patterns are hidden.
|
|
260
|
+
# This matches Parse Server's behavior.
|
|
261
|
+
#
|
|
262
|
+
# @param data [Hash] the data hash to filter
|
|
263
|
+
# @param user [Parse::User, String, nil] the user making the request (or user ID)
|
|
264
|
+
# @param roles [Array<String>] role names the user belongs to
|
|
265
|
+
# @param authenticated [Boolean] whether the user is authenticated (affects "authenticated" pattern)
|
|
266
|
+
# @return [Hash] filtered data with protected fields removed
|
|
267
|
+
#
|
|
268
|
+
# @example Filtering data for a regular user
|
|
269
|
+
# filtered = clp.filter_fields(song_data, user: current_user, roles: ["Member"])
|
|
270
|
+
#
|
|
271
|
+
# @example Filtering data in a webhook
|
|
272
|
+
# # In your webhook handler:
|
|
273
|
+
# clp = Song.fetch_clp
|
|
274
|
+
# filtered_data = clp.filter_fields(
|
|
275
|
+
# response_data,
|
|
276
|
+
# user: request_user,
|
|
277
|
+
# roles: user_roles
|
|
278
|
+
# )
|
|
279
|
+
#
|
|
280
|
+
# @example Filtering with authentication check
|
|
281
|
+
# # Authenticated users may have different visibility
|
|
282
|
+
# clp.filter_fields(data, user: user, roles: roles, authenticated: true)
|
|
283
|
+
def filter_fields(data, user: nil, roles: [], authenticated: nil)
|
|
284
|
+
return data if data.nil?
|
|
285
|
+
return data.map { |item| filter_fields(item, user: user, roles: roles, authenticated: authenticated) } if data.is_a?(Array)
|
|
286
|
+
return data unless data.is_a?(Hash)
|
|
287
|
+
|
|
288
|
+
# Auto-detect authentication if not specified
|
|
289
|
+
authenticated = user.present? if authenticated.nil?
|
|
290
|
+
|
|
291
|
+
# Build list of patterns that apply to this user/context
|
|
292
|
+
applicable_patterns = build_applicable_patterns(user, roles, authenticated, data)
|
|
293
|
+
|
|
294
|
+
# Determine which fields to hide using intersection logic
|
|
295
|
+
fields_to_hide = determine_fields_to_hide(applicable_patterns)
|
|
296
|
+
|
|
297
|
+
# Return filtered data
|
|
298
|
+
data.reject { |key, _| fields_to_hide.include?(key.to_s) }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# The default permission to use for operations not explicitly set.
|
|
302
|
+
# When set, `as_json` will include this for all undefined operations.
|
|
303
|
+
# @return [Hash, nil] the default permission hash (e.g., { "*" => true })
|
|
304
|
+
attr_accessor :default_permission
|
|
305
|
+
|
|
306
|
+
# Default public permission used as fallback when include_defaults is true
|
|
307
|
+
# but no explicit default_permission has been set.
|
|
308
|
+
DEFAULT_PUBLIC_PERMISSION = { "*" => true }.freeze
|
|
309
|
+
|
|
310
|
+
# Convert to Parse Server CLP format.
|
|
311
|
+
#
|
|
312
|
+
# IMPORTANT: Parse Server interprets missing operations as {} (no access).
|
|
313
|
+
# If you have protectedFields but no operations defined, the class becomes
|
|
314
|
+
# effectively master-key-only. Use `set_default_permission` or `include_defaults`
|
|
315
|
+
# to ensure all operations are included.
|
|
316
|
+
#
|
|
317
|
+
# @param include_defaults [Boolean] whether to include default permissions
|
|
318
|
+
# for operations that haven't been explicitly set. When true, uses
|
|
319
|
+
# @default_permission if set, otherwise falls back to public access.
|
|
320
|
+
# @return [Hash] the CLP hash suitable for schema updates
|
|
321
|
+
def as_json(include_defaults: nil)
|
|
322
|
+
result = {}
|
|
323
|
+
|
|
324
|
+
# Determine if we should include defaults
|
|
325
|
+
# Auto-enable if any CLP settings exist and no explicit choice made
|
|
326
|
+
should_include_defaults = if include_defaults.nil?
|
|
327
|
+
present? && @default_permission
|
|
328
|
+
else
|
|
329
|
+
include_defaults
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Determine the default permission to use
|
|
333
|
+
# Use explicit default_permission if set, otherwise fall back to public
|
|
334
|
+
effective_default = @default_permission || DEFAULT_PUBLIC_PERMISSION
|
|
335
|
+
|
|
336
|
+
# Add operation permissions
|
|
337
|
+
OPERATIONS.each do |op|
|
|
338
|
+
if @permissions[op]
|
|
339
|
+
result[op.to_s] = @permissions[op]
|
|
340
|
+
elsif should_include_defaults
|
|
341
|
+
result[op.to_s] = effective_default.dup
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Add pointer permissions (readUserFields, writeUserFields)
|
|
346
|
+
POINTER_PERMISSIONS.each do |perm|
|
|
347
|
+
result[perm.to_s] = @permissions[perm] if @permissions[perm]&.any?
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Add protected fields
|
|
351
|
+
result["protectedFields"] = @protected_fields unless @protected_fields.empty?
|
|
352
|
+
|
|
353
|
+
result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Set the default permission for operations not explicitly configured.
|
|
357
|
+
# This ensures that when CLPs are pushed to Parse Server, all operations
|
|
358
|
+
# have explicit permissions (avoiding the implicit {} = no access behavior).
|
|
359
|
+
#
|
|
360
|
+
# @param public_access [Boolean] whether public access is allowed
|
|
361
|
+
# @param requires_authentication [Boolean] whether authentication is required
|
|
362
|
+
# @param roles [Array<String>] role names that have access
|
|
363
|
+
# @return [self]
|
|
364
|
+
# @example
|
|
365
|
+
# clp.set_default_permission(public_access: true) # Default to public
|
|
366
|
+
# clp.set_default_permission(requires_authentication: true) # Default to auth required
|
|
367
|
+
def set_default_permission(public_access: nil, requires_authentication: false, roles: [])
|
|
368
|
+
perm = {}
|
|
369
|
+
perm["*"] = true if public_access == true
|
|
370
|
+
perm["requiresAuthentication"] = true if requires_authentication
|
|
371
|
+
Array(roles).each { |role| perm["role:#{role}"] = true }
|
|
372
|
+
@default_permission = perm.empty? ? nil : perm
|
|
373
|
+
self
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
alias_method :to_h, :as_json
|
|
377
|
+
|
|
378
|
+
# Check if there are any CLP settings.
|
|
379
|
+
# @return [Boolean]
|
|
380
|
+
def present?
|
|
381
|
+
@permissions.any? || @protected_fields.any?
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Check if this CLP is empty.
|
|
385
|
+
# @return [Boolean]
|
|
386
|
+
def empty?
|
|
387
|
+
!present?
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Merge another CLP into this one (non-destructive).
|
|
391
|
+
# @param other [CLP, Hash] the CLP to merge
|
|
392
|
+
# @return [CLP] a new merged CLP
|
|
393
|
+
def merge(other)
|
|
394
|
+
other_data = other.is_a?(CLP) ? other.as_json : other
|
|
395
|
+
new_clp = CLP.new(as_json)
|
|
396
|
+
new_clp.parse_data(other_data)
|
|
397
|
+
new_clp
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Merge another CLP into this one (destructive).
|
|
401
|
+
# @param other [CLP, Hash] the CLP to merge
|
|
402
|
+
# @return [self]
|
|
403
|
+
def merge!(other)
|
|
404
|
+
other_data = other.is_a?(CLP) ? other.as_json : other
|
|
405
|
+
parse_data(other_data)
|
|
406
|
+
self
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Create a deep copy of this CLP.
|
|
410
|
+
# @return [CLP]
|
|
411
|
+
def dup
|
|
412
|
+
CLP.new(as_json)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Equality check.
|
|
416
|
+
# @param other [CLP, Hash] the other CLP to compare
|
|
417
|
+
# @return [Boolean]
|
|
418
|
+
def ==(other)
|
|
419
|
+
return false unless other.is_a?(CLP) || other.is_a?(Hash)
|
|
420
|
+
as_json == (other.is_a?(CLP) ? other.as_json : other)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def inspect
|
|
424
|
+
"#<Parse::CLP #{as_json.inspect}>"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
private
|
|
428
|
+
|
|
429
|
+
# Build list of patterns that apply to a given user context.
|
|
430
|
+
# All matching patterns will be used for intersection logic.
|
|
431
|
+
#
|
|
432
|
+
# @param user [Parse::User, String, nil] the user or user ID
|
|
433
|
+
# @param roles [Array<String>] role names
|
|
434
|
+
# @param authenticated [Boolean] whether user is authenticated
|
|
435
|
+
# @param data [Hash] the data being filtered (for userField checks)
|
|
436
|
+
# @return [Array<String>] all applicable patterns
|
|
437
|
+
def build_applicable_patterns(user, roles, authenticated, data)
|
|
438
|
+
patterns = []
|
|
439
|
+
user_id = extract_user_id(user)
|
|
440
|
+
|
|
441
|
+
# Check userField patterns (owner-based access)
|
|
442
|
+
@protected_fields.keys.each do |pattern|
|
|
443
|
+
next unless pattern.start_with?("userField:")
|
|
444
|
+
|
|
445
|
+
field_name = pattern.sub("userField:", "")
|
|
446
|
+
next unless data.key?(field_name) || data.key?(field_name.to_sym)
|
|
447
|
+
|
|
448
|
+
# Get the field value (could be string key or symbol key)
|
|
449
|
+
field_value = data[field_name] || data[field_name.to_sym]
|
|
450
|
+
|
|
451
|
+
if user_id && user_matches_field?(user_id, field_value)
|
|
452
|
+
patterns << pattern
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Add role patterns for all roles the user belongs to
|
|
457
|
+
Array(roles).each do |role|
|
|
458
|
+
role_pattern = role.start_with?("role:") ? role : "role:#{role}"
|
|
459
|
+
patterns << role_pattern if @protected_fields.key?(role_pattern)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Add user-specific pattern if configured
|
|
463
|
+
if user_id && @protected_fields.key?(user_id)
|
|
464
|
+
patterns << user_id
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Add "authenticated" pattern if user is authenticated and pattern exists
|
|
468
|
+
if authenticated && @protected_fields.key?("authenticated")
|
|
469
|
+
patterns << "authenticated"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Public pattern "*" always applies (for everyone)
|
|
473
|
+
patterns << "*" if @protected_fields.key?("*")
|
|
474
|
+
|
|
475
|
+
patterns
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Extract user ID from various user representations.
|
|
479
|
+
# @param user [Parse::User, String, Hash, nil] user object, ID, or pointer hash
|
|
480
|
+
# @return [String, nil] the user ID or nil
|
|
481
|
+
def extract_user_id(user)
|
|
482
|
+
return nil if user.nil?
|
|
483
|
+
return user if user.is_a?(String)
|
|
484
|
+
return user["objectId"] if user.is_a?(Hash) && user["objectId"]
|
|
485
|
+
return user[:objectId] if user.is_a?(Hash) && user[:objectId]
|
|
486
|
+
return user.id if user.respond_to?(:id)
|
|
487
|
+
nil
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Check if a user ID matches a field value (pointer or array of pointers).
|
|
491
|
+
# @param user_id [String] the user ID to check
|
|
492
|
+
# @param field_value [Hash, Array, String, nil] the field value
|
|
493
|
+
# @return [Boolean] true if the user matches
|
|
494
|
+
def user_matches_field?(user_id, field_value)
|
|
495
|
+
return false if field_value.nil? || user_id.nil?
|
|
496
|
+
|
|
497
|
+
# Handle array of pointers (e.g., owners: [user1, user2])
|
|
498
|
+
if field_value.is_a?(Array)
|
|
499
|
+
return field_value.any? { |item| user_matches_field?(user_id, item) }
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Handle pointer hash (e.g., owner: { __type: "Pointer", objectId: "xxx" })
|
|
503
|
+
if field_value.is_a?(Hash)
|
|
504
|
+
return field_value["objectId"] == user_id || field_value[:objectId] == user_id
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Handle direct ID string
|
|
508
|
+
field_value.to_s == user_id
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Determine which fields should be hidden based on applicable patterns.
|
|
512
|
+
#
|
|
513
|
+
# Uses **intersection** logic: a field is hidden only if it's protected
|
|
514
|
+
# by ALL matching patterns. This matches Parse Server behavior.
|
|
515
|
+
#
|
|
516
|
+
# An empty array `[]` for any matching pattern means "no fields protected"
|
|
517
|
+
# for that pattern, which clears protection (intersection with empty = empty).
|
|
518
|
+
#
|
|
519
|
+
# @param patterns [Array<String>] all applicable patterns
|
|
520
|
+
# @return [Set<String>] field names to hide
|
|
521
|
+
def determine_fields_to_hide(patterns)
|
|
522
|
+
# If no patterns match, no fields are hidden
|
|
523
|
+
return Set.new if patterns.empty?
|
|
524
|
+
|
|
525
|
+
# Get protected fields for each matching pattern
|
|
526
|
+
field_sets = patterns.map do |pattern|
|
|
527
|
+
fields = @protected_fields[pattern]
|
|
528
|
+
# Convert to Set for intersection operations
|
|
529
|
+
# Empty array means "no protection" -> empty set
|
|
530
|
+
fields.nil? ? nil : Set.new(fields)
|
|
531
|
+
end.compact
|
|
532
|
+
|
|
533
|
+
# If any pattern has no configuration, ignore it
|
|
534
|
+
return Set.new if field_sets.empty?
|
|
535
|
+
|
|
536
|
+
# If any pattern explicitly allows all fields (empty array),
|
|
537
|
+
# then the intersection is empty (no fields hidden)
|
|
538
|
+
return Set.new if field_sets.any?(&:empty?)
|
|
539
|
+
|
|
540
|
+
# Intersect all field sets - only fields protected by ALL patterns are hidden
|
|
541
|
+
field_sets.reduce { |result, fields| result & fields }
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|