Class: Parse::CLP

Inherits:
Object
  • Object
show all
Defined in:
lib/parse/model/clp.rb

Overview

Class-Level Permissions (CLP) for Parse Server classes.

CLPs control access to a class at the schema level, determining who can perform operations on the class and which fields are visible to different users/roles.

## Protected Fields Behavior

When a user matches multiple patterns (e.g., public “*”, “authenticated”, and a role), the protected fields are the intersection of all matching patterns. This means a field is only hidden if it’s protected by ALL patterns that apply to the user.

For example:

  • ‘*` protects [“owner”, “test”]

  • ‘role:Admin` protects [“owner”]

  • A user with Admin role matches both patterns

  • Result: only “owner” is hidden (intersection), “test” is visible

An empty array ‘[]` for a pattern means “no fields protected” (user sees everything). If any matching pattern has an empty array, the intersection will also be empty.

Examples:

Defining CLPs in a model

class Song < Parse::Object
  property :title, :string
  property :artist, :string
  property :internal_notes, :string  # Should be hidden from regular users

  # Set class-level permissions
  set_clp :find, public: true
  set_clp :get, public: true
  set_clp :create, public: false, roles: ["Admin", "Editor"]
  set_clp :update, public: false, roles: ["Admin", "Editor"]
  set_clp :delete, public: false, roles: ["Admin"]

  # Protect fields from certain users
  protect_fields "*", [:internal_notes, :secret_data]  # Hidden from everyone
  protect_fields "role:Admin", []  # Admins can see everything
end

Using userField for owner-based access

class Document < Parse::Object
  property :content, :string
  property :secret, :string
  belongs_to :owner, as: :user

  # Hide secret from everyone
  protect_fields "*", [:secret, :owner]
  # But owners can see their own document's secret
  protect_fields "userField:owner", []
end

Fetching CLPs from server

clp = Song.fetch_clp
clp.find_allowed?("role:Admin")  # => true
clp.protected_fields_for("*")    # => ["internal_notes", "secret_data"]

See Also:

Constant Summary collapse

OPERATIONS =

Valid CLP operation keys for permission-based access

%i[find get count create update delete addField].freeze
POINTER_PERMISSIONS =

Pointer-permission keys (users in these fields get read/write access)

%i[readUserFields writeUserFields].freeze
ALL_KEYS =

All valid CLP keys

(OPERATIONS + POINTER_PERMISSIONS + [:protectedFields]).freeze
DEFAULT_PUBLIC_PERMISSION =

Default public permission used as fallback when include_defaults is true but no explicit default_permission has been set.

{ "*" => true }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data = nil) ⇒ CLP

Create a new CLP instance.

Parameters:

  • data (Hash) (defaults to: nil)

    optional initial CLP data from Parse Server



77
78
79
80
81
# File 'lib/parse/model/clp.rb', line 77

def initialize(data = nil)
  @permissions = {}
  @protected_fields = {}
  parse_data(data) if data.is_a?(Hash)
end

Instance Attribute Details

#default_permissionHash?

The default permission to use for operations not explicitly set. When set, ‘as_json` will include this for all undefined operations.

Returns:

  • (Hash, nil)

    the default permission hash (e.g., { “*” => true })



304
305
306
# File 'lib/parse/model/clp.rb', line 304

def default_permission
  @default_permission
end

#permissionsHash (readonly)

Returns the raw CLP hash.

Returns:

  • (Hash)

    the raw CLP hash



73
74
75
# File 'lib/parse/model/clp.rb', line 73

def permissions
  @permissions
end

Instance Method Details

#==(other) ⇒ Boolean

Equality check.

Parameters:

  • other (CLP, Hash)

    the other CLP to compare

Returns:

  • (Boolean)


418
419
420
421
# File 'lib/parse/model/clp.rb', line 418

def ==(other)
  return false unless other.is_a?(CLP) || other.is_a?(Hash)
  as_json == (other.is_a?(CLP) ? other.as_json : other)
end

#allowed?(operation, pattern) ⇒ Boolean

Check if a specific pattern has access to an operation.

Parameters:

  • operation (Symbol)

    the operation to check

  • pattern (String)

    the pattern (“*”, “role:RoleName”, user objectId)

Returns:

  • (Boolean)


212
213
214
215
216
217
218
219
220
221
# File 'lib/parse/model/clp.rb', line 212

def allowed?(operation, pattern)
  perm = @permissions[operation.to_sym]
  return false unless perm

  # Check direct access
  return true if perm[pattern.to_s] == true
  return true if perm["*"] == true

  false
end

#as_json(include_defaults: nil) ⇒ Hash Also known as: to_h

Convert to Parse Server CLP format.

IMPORTANT: Parse Server interprets missing operations as {} (no access). If you have protectedFields but no operations defined, the class becomes effectively master-key-only. Use ‘set_default_permission` or `include_defaults` to ensure all operations are included.

Parameters:

  • include_defaults (Boolean) (defaults to: nil)

    whether to include default permissions for operations that haven’t been explicitly set. When true, uses @default_permission if set, otherwise falls back to public access.

Returns:

  • (Hash)

    the CLP hash suitable for schema updates



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/parse/model/clp.rb', line 321

def as_json(include_defaults: nil)
  result = {}

  # Determine if we should include defaults
  # Auto-enable if any CLP settings exist and no explicit choice made
  should_include_defaults = if include_defaults.nil?
    present? && @default_permission
  else
    include_defaults
  end

  # Determine the default permission to use
  # Use explicit default_permission if set, otherwise fall back to public
  effective_default = @default_permission || DEFAULT_PUBLIC_PERMISSION

  # Add operation permissions
  OPERATIONS.each do |op|
    if @permissions[op]
      result[op.to_s] = @permissions[op]
    elsif should_include_defaults
      result[op.to_s] = effective_default.dup
    end
  end

  # Add pointer permissions (readUserFields, writeUserFields)
  POINTER_PERMISSIONS.each do |perm|
    result[perm.to_s] = @permissions[perm] if @permissions[perm]&.any?
  end

  # Add protected fields
  result["protectedFields"] = @protected_fields unless @protected_fields.empty?

  result
end

#dupCLP

Create a deep copy of this CLP.

Returns:



411
412
413
# File 'lib/parse/model/clp.rb', line 411

def dup
  CLP.new(as_json)
end

#empty?Boolean

Check if this CLP is empty.

Returns:

  • (Boolean)


386
387
388
# File 'lib/parse/model/clp.rb', line 386

def empty?
  !present?
end

#filter_fields(data, user: nil, roles: [], authenticated: nil) ⇒ Hash

Filter fields from a hash based on protected fields for a user/role. This is the core method for filtering webhook responses.

Uses intersection logic: when a user matches multiple patterns, only fields that are protected by ALL matching patterns are hidden. This matches Parse Server’s behavior.

Examples:

Filtering data for a regular user

filtered = clp.filter_fields(song_data, user: current_user, roles: ["Member"])

Filtering data in a webhook

# In your webhook handler:
clp = Song.fetch_clp
filtered_data = clp.filter_fields(
  response_data,
  user: request_user,
  roles: user_roles
)

Filtering with authentication check

# Authenticated users may have different visibility
clp.filter_fields(data, user: user, roles: roles, authenticated: true)

Parameters:

  • data (Hash)

    the data hash to filter

  • user (Parse::User, String, nil) (defaults to: nil)

    the user making the request (or user ID)

  • roles (Array<String>) (defaults to: [])

    role names the user belongs to

  • authenticated (Boolean) (defaults to: nil)

    whether the user is authenticated (affects “authenticated” pattern)

Returns:

  • (Hash)

    filtered data with protected fields removed



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/parse/model/clp.rb', line 283

def filter_fields(data, user: nil, roles: [], authenticated: nil)
  return data if data.nil?
  return data.map { |item| filter_fields(item, user: user, roles: roles, authenticated: authenticated) } if data.is_a?(Array)
  return data unless data.is_a?(Hash)

  # Auto-detect authentication if not specified
  authenticated = user.present? if authenticated.nil?

  # Build list of patterns that apply to this user/context
  applicable_patterns = build_applicable_patterns(user, roles, authenticated, data)

  # Determine which fields to hide using intersection logic
  fields_to_hide = determine_fields_to_hide(applicable_patterns)

  # Return filtered data
  data.reject { |key, _| fields_to_hide.include?(key.to_s) }
end

#inspectObject



423
424
425
# File 'lib/parse/model/clp.rb', line 423

def inspect
  "#<Parse::CLP #{as_json.inspect}>"
end

#merge(other) ⇒ CLP

Merge another CLP into this one (non-destructive).

Parameters:

  • other (CLP, Hash)

    the CLP to merge

Returns:

  • (CLP)

    a new merged CLP



393
394
395
396
397
398
# File 'lib/parse/model/clp.rb', line 393

def merge(other)
  other_data = other.is_a?(CLP) ? other.as_json : other
  new_clp = CLP.new(as_json)
  new_clp.parse_data(other_data)
  new_clp
end

#merge!(other) ⇒ self

Merge another CLP into this one (destructive).

Parameters:

  • other (CLP, Hash)

    the CLP to merge

Returns:

  • (self)


403
404
405
406
407
# File 'lib/parse/model/clp.rb', line 403

def merge!(other)
  other_data = other.is_a?(CLP) ? other.as_json : other
  parse_data(other_data)
  self
end

#parse_data(data) ⇒ Object

Parse CLP data from Parse Server format.

Parameters:

  • data (Hash)

    CLP hash from server



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/parse/model/clp.rb', line 85

def parse_data(data)
  data.each do |key, value|
    key_sym = key.to_sym
    if key_sym == :protectedFields
      @protected_fields = value.transform_keys(&:to_s)
    elsif OPERATIONS.include?(key_sym)
      @permissions[key_sym] = value.transform_keys(&:to_s)
    elsif POINTER_PERMISSIONS.include?(key_sym)
      # readUserFields and writeUserFields are arrays of field names
      @permissions[key_sym] = Array(value)
    else
      # Store any other keys
      @permissions[key_sym] = value
    end
  end
end

#present?Boolean

Check if there are any CLP settings.

Returns:

  • (Boolean)


380
381
382
# File 'lib/parse/model/clp.rb', line 380

def present?
  @permissions.any? || @protected_fields.any?
end

#protected_fieldsHash

Get all protected fields configuration.

Returns:

  • (Hash)

    pattern => [fields] mapping (deep copy)



204
205
206
# File 'lib/parse/model/clp.rb', line 204

def protected_fields
  @protected_fields.transform_values(&:dup)
end

#protected_fields_for(pattern) ⇒ Array<String>

Get protected fields for a specific pattern.

Parameters:

  • pattern (String)

    the pattern to look up

Returns:



198
199
200
# File 'lib/parse/model/clp.rb', line 198

def protected_fields_for(pattern)
  @protected_fields[pattern.to_s] || []
end

#public_access?(operation) ⇒ Boolean

Check if public access is allowed for an operation.

Parameters:

  • operation (Symbol)

    the operation to check

Returns:

  • (Boolean)


226
227
228
# File 'lib/parse/model/clp.rb', line 226

def public_access?(operation)
  allowed?(operation, "*")
end

#read_user_fieldsArray<String>

Get the read user fields.

Returns:

  • (Array<String>)

    pointer field names for read access



126
127
128
# File 'lib/parse/model/clp.rb', line 126

def read_user_fields
  @permissions[:readUserFields] || []
end

#requires_authentication?(operation) ⇒ Boolean

Check if authentication is required for an operation.

Parameters:

  • operation (Symbol)

    the operation to check

Returns:

  • (Boolean)


249
250
251
252
253
# File 'lib/parse/model/clp.rb', line 249

def requires_authentication?(operation)
  perm = @permissions[operation.to_sym]
  return false unless perm
  perm["requiresAuthentication"] == true
end

#role_allowed?(operation, role_name) ⇒ Boolean

Check if a role has access to an operation.

Parameters:

  • operation (Symbol)

    the operation to check

  • role_name (String)

    the role name (with or without “role:” prefix)

Returns:

  • (Boolean)


234
235
236
237
# File 'lib/parse/model/clp.rb', line 234

def role_allowed?(operation, role_name)
  role_key = role_name.start_with?("role:") ? role_name : "role:#{role_name}"
  allowed?(operation, role_key)
end

#set_default_permission(public_access: nil, requires_authentication: false, roles: []) ⇒ self

Set the default permission for operations not explicitly configured. This ensures that when CLPs are pushed to Parse Server, all operations have explicit permissions (avoiding the implicit {} = no access behavior).

Examples:

clp.set_default_permission(public_access: true)  # Default to public
clp.set_default_permission(requires_authentication: true)  # Default to auth required

Parameters:

  • public_access (Boolean) (defaults to: nil)

    whether public access is allowed

  • requires_authentication (Boolean) (defaults to: false)

    whether authentication is required

  • roles (Array<String>) (defaults to: [])

    role names that have access

Returns:

  • (self)


367
368
369
370
371
372
373
374
# File 'lib/parse/model/clp.rb', line 367

def set_default_permission(public_access: nil, requires_authentication: false, roles: [])
  perm = {}
  perm["*"] = true if public_access == true
  perm["requiresAuthentication"] = true if requires_authentication
  Array(roles).each { |role| perm["role:#{role}"] = true }
  @default_permission = perm.empty? ? nil : perm
  self
end

#set_permission(operation, public_access: nil, roles: [], users: [], pointer_fields: [], requires_authentication: false) ⇒ self

Set permissions for a specific operation.

Parameters:

  • operation (Symbol)

    one of :find, :get, :count, :create, :update, :delete, :addField

  • public_access (Boolean, nil) (defaults to: nil)

    whether public access is allowed

  • roles (Array<String>) (defaults to: [])

    role names that have access

  • users (Array<String>) (defaults to: [])

    user objectIds that have access

  • pointer_fields (Array<String>) (defaults to: [])

    pointer field names for userField access

  • requires_authentication (Boolean) (defaults to: false)

    whether authentication is required

Returns:

  • (self)

Raises:

  • (ArgumentError)


144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/parse/model/clp.rb', line 144

def set_permission(operation, public_access: nil, roles: [], users: [], pointer_fields: [], requires_authentication: false)
  operation = operation.to_sym
  raise ArgumentError, "Invalid operation: #{operation}" unless OPERATIONS.include?(operation)

  perm = {}

  # Handle public access
  # Note: Parse Server only accepts 'true' values for CLP permissions.
  # Setting public: false means "don't grant public access" which is
  # achieved by simply not including the "*" key (absence = no access).
  perm["*"] = true if public_access == true

  # Handle requiresAuthentication
  perm["requiresAuthentication"] = true if requires_authentication

  # Handle roles
  Array(roles).each do |role|
    role_key = role.start_with?("role:") ? role : "role:#{role}"
    perm[role_key] = true
  end

  # Handle users
  Array(users).each do |user_id|
    perm[user_id] = true
  end

  # Handle pointer fields (userField:fieldName pattern)
  Array(pointer_fields).each do |field|
    field_key = field.start_with?("pointerFields") ? field : "pointerFields"
    perm[field_key] ||= []
    perm[field_key] << field unless field.start_with?("pointerFields")
  end

  @permissions[operation] = perm
  self
end

#set_protected_fields(pattern, fields) ⇒ self

Set protected fields for a specific user/role pattern.

Examples:

clp.set_protected_fields("*", [:email, :phone])  # Hide from everyone
clp.set_protected_fields("role:Admin", [])       # Admins see everything
clp.set_protected_fields("userField:owner", [])  # Owners see everything

Parameters:

  • pattern (String)

    the pattern (“*”, “role:RoleName”, “userField:fieldName”, or user objectId)

  • fields (Array<String, Symbol>)

    field names to protect (hide) from this pattern

Returns:

  • (self)


189
190
191
192
193
# File 'lib/parse/model/clp.rb', line 189

def set_protected_fields(pattern, fields)
  pattern = "*" if pattern.to_sym == :public rescue pattern
  @protected_fields[pattern.to_s] = Array(fields).map(&:to_s)
  self
end

#set_read_user_fields(*fields) ⇒ self

Set pointer-permission fields for read access. Users pointed to by these fields can read the object.

Examples:

clp.set_read_user_fields(:owner, :collaborators)

Parameters:

Returns:

  • (self)


108
109
110
111
# File 'lib/parse/model/clp.rb', line 108

def set_read_user_fields(*fields)
  @permissions[:readUserFields] = fields.flatten.map(&:to_s)
  self
end

#set_write_user_fields(*fields) ⇒ self

Set pointer-permission fields for write access. Users pointed to by these fields can write to the object.

Examples:

clp.set_write_user_fields(:owner)

Parameters:

Returns:

  • (self)


119
120
121
122
# File 'lib/parse/model/clp.rb', line 119

def set_write_user_fields(*fields)
  @permissions[:writeUserFields] = fields.flatten.map(&:to_s)
  self
end

#write_user_fieldsArray<String>

Get the write user fields.

Returns:

  • (Array<String>)

    pointer field names for write access



132
133
134
# File 'lib/parse/model/clp.rb', line 132

def write_user_fields
  @permissions[:writeUserFields] || []
end