Skip to content

Commit ce528d7

Browse files
committed
Add default and pointer permissions to CLP DSL
Enhances the Parse::CLP class and Parse::Object DSL to support default permissions for all operations, pointer-based permissions (readUserFields, writeUserFields), and improved snake_case to camelCase field conversion. Updates integration and unit tests to cover new features, including default permission propagation, pointer permissions, and field name conversions.
1 parent cbba708 commit ce528d7

6 files changed

Lines changed: 938 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,48 @@
22

33
### 3.2.1
44

5+
#### New Features
6+
7+
- **NEW**: Added `set_default_clp` method to set a default permission for all CLP operations at once. This is important because Parse Server treats missing operations as `{}` (no access, master key only).
8+
9+
```ruby
10+
class Document < Parse::Object
11+
# Set all operations to public by default
12+
set_default_clp public: true
13+
14+
# Or require authentication for all operations
15+
set_default_clp requires_authentication: true
16+
17+
# Or restrict all operations to specific roles
18+
set_default_clp roles: ["Admin", "Editor"]
19+
20+
# Then override specific operations as needed
21+
set_clp :delete, public: false, roles: ["Admin"]
22+
end
23+
```
24+
25+
- **NEW**: Added `set_read_user_fields` and `set_write_user_fields` for pointer-based permissions. These allow users referenced by pointer fields to have read/write access to objects.
26+
27+
```ruby
28+
class Document < Parse::Object
29+
belongs_to :owner, as: :user
30+
belongs_to :editor, as: :user
31+
32+
# Owner can read, editor can write
33+
set_read_user_fields [:owner]
34+
set_write_user_fields [:editor]
35+
36+
# Snake_case field names are auto-converted to camelCase
37+
end
38+
```
39+
40+
- **NEW**: Added `reset_clp!` method to reset CLPs to public defaults. Useful for clearing restrictive permissions that may have accumulated on the server.
41+
42+
```ruby
43+
# Reset all CLPs to public access
44+
Song.reset_clp!
45+
```
46+
547
#### Improvements
648

749
- **IMPROVED**: CLP methods now automatically convert snake_case Ruby property names to camelCase Parse Server field names. This provides consistency with the rest of the Parse Stack framework where you define properties in snake_case.
@@ -12,7 +54,7 @@
1254
class Document < Parse::Object
1355
property :internal_notes, :string
1456
property :secret_data, :string
15-
property :owner_user, :pointer, as: :user
57+
belongs_to :owner_user, as: :user
1658

1759
# Field names are auto-converted
1860
protect_fields "*", [:internal_notes, :secret_data]
@@ -33,17 +75,35 @@ end
3375

3476
```ruby
3577
class Document < Parse::Object
36-
property :owner_field, :pointer, as: :user
37-
property :editor_field, :pointer, as: :user
78+
belongs_to :owner_field, as: :user
79+
belongs_to :editor_field, as: :user
3880

3981
# pointer_fields are auto-converted
4082
set_clp :update, pointer_fields: [:owner_field, :editor_field]
4183
# Converts to: pointerFields: ["ownerField", "editorField"]
4284
end
4385
```
4486

87+
- **IMPROVED**: Added `include_defaults` parameter to `CLP#as_json`. When `true`, includes default permissions for all undefined operations (useful when pushing complete CLP to server).
88+
89+
```ruby
90+
clp = Parse::CLP.new
91+
clp.set_default_permission(public: true)
92+
clp.set_permission(:delete, roles: ["Admin"])
93+
94+
# Without defaults - only explicitly set operations
95+
clp.as_json
96+
# => {"delete" => {"role:Admin" => true}}
97+
98+
# With defaults - all operations included
99+
clp.as_json(include_defaults: true)
100+
# => {"find" => {"*" => true}, "get" => {"*" => true}, ... "delete" => {"role:Admin" => true}}
101+
```
102+
45103
#### Bug Fixes
46104

105+
- **FIXED**: `auto_upgrade!` now resets CLPs before applying new ones. Parse Server merges CLP updates rather than replacing them, so old restrictive permissions could persist and cause "Permission denied" errors. Now `auto_upgrade!` first resets CLPs to public defaults, then applies the model's CLP configuration.
106+
47107
- **FIXED**: Test setup for role membership now correctly uses `add_users()` method for adding users to roles (roles use Parse Relations, not Array properties).
48108

49109
### 3.2.0

lib/parse/model/clp.rb

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,15 @@ module Parse
6060
#
6161
# @see https://docs.parseplatform.org/rest/guide/#class-level-permissions
6262
class CLP
63-
# Valid CLP operation keys
63+
# Valid CLP operation keys for permission-based access
6464
OPERATIONS = %i[find get count create update delete addField].freeze
6565

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+
6672
# @return [Hash] the raw CLP hash
6773
attr_reader :permissions
6874

@@ -83,13 +89,50 @@ def parse_data(data)
8389
@protected_fields = value.transform_keys(&:to_s)
8490
elsif OPERATIONS.include?(key_sym)
8591
@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)
8695
else
87-
# Store any other keys (like requiresAuthentication, etc.)
96+
# Store any other keys
8897
@permissions[key_sym] = value
8998
end
9099
end
91100
end
92101

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+
93136
# Set permissions for a specific operation.
94137
# @param operation [Symbol] one of :find, :get, :count, :create, :update, :delete, :addField
95138
# @param public_access [Boolean, nil] whether public access is allowed
@@ -255,14 +298,53 @@ def filter_fields(data, user: nil, roles: [], authenticated: nil)
255298
data.reject { |key, _| fields_to_hide.include?(key.to_s) }
256299
end
257300

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+
258310
# 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.
259320
# @return [Hash] the CLP hash suitable for schema updates
260-
def as_json(*_args)
321+
def as_json(include_defaults: nil)
261322
result = {}
262323

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+
263336
# Add operation permissions
264337
OPERATIONS.each do |op|
265-
result[op.to_s] = @permissions[op] if @permissions[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?
266348
end
267349

268350
# Add protected fields
@@ -271,6 +353,26 @@ def as_json(*_args)
271353
result
272354
end
273355

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+
274376
alias_method :to_h, :as_json
275377

276378
# Check if there are any CLP settings.

lib/parse/model/core/schema.rb

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ def fetch_schema
7373
Parse::Model::CLASS_SCHEMA
7474
].freeze
7575

76+
# Default CLP that grants public access to all operations.
77+
# Used to reset CLPs before applying new ones.
78+
DEFAULT_PUBLIC_CLP = {
79+
"find" => { "*" => true },
80+
"get" => { "*" => true },
81+
"count" => { "*" => true },
82+
"create" => { "*" => true },
83+
"update" => { "*" => true },
84+
"delete" => { "*" => true },
85+
"addField" => { "*" => true }
86+
}.freeze
87+
88+
# Reset the CLP on the server to public defaults.
89+
# This clears any existing restrictive permissions.
90+
#
91+
# @param client [Parse::Client] optional client to use
92+
# @return [Parse::Response] the response from the server
93+
#
94+
# @example Reset CLPs to public
95+
# Song.reset_clp!
96+
def reset_clp!(client: nil)
97+
client ||= self.client
98+
99+
unless client.master_key.present?
100+
warn "[Parse] CLP reset for #{parse_class} requires the master key!"
101+
return nil
102+
end
103+
104+
client.update_schema(parse_class, { "classLevelPermissions" => DEFAULT_PUBLIC_CLP })
105+
end
106+
76107
# A class method for non-destructive auto upgrading a remote schema based
77108
# on the properties and relations you have defined in your local model. If
78109
# the collection doesn't exist, we create the schema. If the collection already
@@ -123,9 +154,15 @@ def auto_upgrade!(include_clp: true)
123154
h
124155
end
125156

126-
# Add CLP updates if configured and requested
157+
# Handle CLP updates if configured and requested
127158
if include_clp && respond_to?(:class_permissions) && class_permissions.present?
128-
current_schema[:classLevelPermissions] = class_permissions.as_json
159+
# First, reset CLPs to public defaults to clear any old restrictive permissions.
160+
# Parse Server merges CLPs rather than replacing them, so old keys can persist
161+
# and cause "Permission denied" errors if not explicitly cleared.
162+
reset_clp!
163+
164+
# Now apply the new CLP configuration
165+
current_schema[:classLevelPermissions] = class_permissions.as_json(include_defaults: true)
129166
end
130167

131168
return true if current_schema[:fields].empty? && !current_schema[:classLevelPermissions]
@@ -136,7 +173,7 @@ def auto_upgrade!(include_clp: true)
136173
initial_schema = schema
137174
# Include CLPs in initial schema creation if configured
138175
if include_clp && respond_to?(:class_permissions) && class_permissions.present?
139-
initial_schema[:classLevelPermissions] = class_permissions.as_json
176+
initial_schema[:classLevelPermissions] = class_permissions.as_json(include_defaults: true)
140177
end
141178
client.create_schema parse_class, initial_schema
142179
end

0 commit comments

Comments
 (0)