Module: Parse::Core::FieldGuards

Extended by:
ActiveSupport::Concern
Included in:
Object
Defined in:
lib/parse/model/core/field_guards.rb

Overview

Declarative write protection for model fields, enforced inside before_save webhook handling. Unlike Parse Server’s class-level ‘protectedFields` (which only hides values on read), these guards revert disallowed client writes before the change reaches the persistent store.

Four modes are supported:

  • ‘:master_only` - the field is never writable by clients. Any client-supplied value is reverted; master-key requests bypass the guard.

  • ‘:immutable` - the field is writable when the object is created but is reverted on any subsequent client update. Master-key requests bypass the guard.

  • ‘:always_immutable` - same as `:immutable` for creates, but the field is also reverted on master-key updates. Useful for fields that must NEVER change after creation regardless of who is writing (e.g. a one-way state transition marker, or a slug used in canonical URLs that breaks on rename).

  • ‘:set_once` - the field is writable while the persisted value is blank, then locked forever. Master-key writes DO NOT bypass the lock once a value is set. Useful for derived fields that are populated by an after_create callback (e.g. `parse_reference`) where the canonical value depends on the server-assigned objectId and must never change after first assignment.

Reverts are a silent successful no-op from the client’s perspective: the save proceeds normally, the guarded field simply isn’t written. A DEBUG-level log line is emitted for diagnosis, but nothing is raised and nothing is logged at WARN/INFO, so clients that routinely resubmit a full record don’t generate log noise.

Examples:

class Project < Parse::Object
  property :slug, :string
  property :created_by, :pointer

  guard :created_by, :master_only
  guard :slug, :external_id, :immutable
end

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

GUARD_MODES =
[:master_only, :immutable, :always_immutable, :set_once].freeze

Instance Method Summary collapse

Instance Method Details

#apply_field_guards!(master:, is_new:) ⇒ Array<Symbol>

Revert any disallowed client writes per the class-level guards. Called by Webhooks.call_route for before_save triggers, before Object#prepare_save! runs.

Parameters:

  • master (Boolean)

    true if the webhook request used the master key

  • is_new (Boolean)

    true if this is a create (no original record)

Returns:



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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
# File 'lib/parse/model/core/field_guards.rb', line 125

def apply_field_guards!(master:, is_new:)
  guards = self.class.field_guards
  return [] if guards.blank?

  reverted = guards.each_with_object([]) do |(field, mode), acc|
    next unless changed.include?(field.to_s)
    case mode
    when :master_only
      # Master bypasses; client writes always reverted
      next if master
      revert_field!(field, is_new: is_new)
      acc << field
    when :immutable
      # Master bypasses; clients can set on create, never on update
      next if master
      next if is_new
      revert_field!(field, is_new: false)
      acc << field
    when :always_immutable
      # No master bypass on updates: the field is frozen for everyone
      # (including server/admin code using the master key) once the
      # object exists. Creates are still allowed for everyone.
      next if is_new
      revert_field!(field, is_new: false)
      acc << field
    when :set_once
      # Allow writes while the persisted (original) value is blank;
      # lock the field once it holds a value. No master bypass --
      # once set, NOTHING can change it. Implementation note: this
      # checks the dirty-tracked "was" value rather than the current
      # value, so an update payload that includes a new value is
      # only rejected if the field was previously populated.
      previous = changed_attributes[field.to_s]
      next if previous.nil? || previous.to_s.strip.empty?
      revert_field!(field, is_new: false)
      acc << field
    end
  end

  if reverted.any?
    klass = self.class.respond_to?(:parse_class) ? self.class.parse_class : self.class.name
    oid = (respond_to?(:id) && id) || "<new>"
    Parse.logger&.debug(
      "[Parse::FieldGuards] Reverted client writes on #{klass}:#{oid} -> #{reverted.join(", ")}"
    )
  end

  reverted
end