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.
Defined Under Namespace
Modules: ClassMethods
Constant Summary collapse
- GUARD_MODES =
[:master_only, :immutable, :always_immutable, :set_once].freeze
Instance Method Summary collapse
-
#apply_field_guards!(master:, is_new:) ⇒ Array<Symbol>
Revert any disallowed client writes per the class-level guards.
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.
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 |