Class: Parse::Model::Builder

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

Overview

This class provides a method to automatically generate Parse::Object subclasses, including their properties and inferred associations by importing the schema for the remote collections in a Parse application.

Constant Summary collapse

VALID_CLASS_NAME =

Regex matching className strings safe to install as a Ruby constant. Server-returned className must satisfy this; otherwise we refuse to touch the global namespace.

/\A_?[A-Za-z][A-Za-z0-9_]{0,127}\z/.freeze
PROTECTED_SYSTEM_CLASSES =

Parse Server system classes that ship with the SDK as hand-written subclasses (Parse::User, Parse::Role, etc.). Schema-driven builds must NOT install additional fields or associations on these — a compromised Parse Server could otherwise inject an is_admin property onto the real Parse::User class, or a password_history accessor onto _Session, by returning a poisoned schema.

%w[
  _User _Role _Session _Installation _Product _Audience _PushStatus
  _JobStatus _JobSchedule _Hooks _GlobalConfig _SCHEMA _GraphQLConfig
  _Idempotency _Audit
].freeze

Class Method Summary collapse

Class Method Details

.build!(schema) ⇒ Array

Builds a ruby Parse::Object subclass with the provided schema information.

Parameters:

  • schema (Hash)

    the Parse-formatted hash schema for a collection. This hash should two keys:

    • className: Contains the name of the collection.

    • field: A hash containg the column fields and their type.

Returns:

  • (Array)

    an array of Parse::Object subclass constants.

Raises:

  • ArgumentError when the className could not be inferred from the schema.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/parse/model/core/builder.rb', line 58

def self.build!(schema)
  unless schema.is_a?(Hash)
    raise ArgumentError, "Schema parameter should be a Parse schema hash object."
  end
  schema = schema.with_indifferent_access
  fields = schema[:fields] || {}
  className = schema[:className]

  if className.blank?
    raise ArgumentError, "No valid className provided for schema hash"
  end

  # Strictly validate the server-returned className before any constant
  # resolution. This blocks schema-poisoning attacks where a malicious
  # or compromised Parse Server returns a className like "File",
  # "Kernel", or "../foo" intending to either rebind a Ruby built-in
  # constant via const_set or trigger arbitrary autoload via const_get.
  parse_class_name = className.to_parse_class
  unless parse_class_name.is_a?(String) && parse_class_name =~ VALID_CLASS_NAME
    raise ArgumentError, "Unsafe className from schema: #{className.inspect}"
  end

  # Prefer the registered Parse::Object descendant lookup (never touches
  # top-level constants). Only fall back to constant lookup within the
  # Parse::Generated namespace, never on ::Object.
  klass = Parse::Model.find_class(className)
  if klass.nil?
    if Parse::Generated.const_defined?(parse_class_name, false)
      klass = Parse::Generated.const_get(parse_class_name, false)
    end
  end
  if klass.nil?
    klass = ::Class.new(Parse::Object)
    Parse::Generated.const_set(parse_class_name, klass)
  end
  unless klass.is_a?(Class) && klass <= Parse::Object
    raise ArgumentError, "Resolved class #{klass.inspect} for #{className.inspect} is not a Parse::Object subclass"
  end

  # Refuse to install schema-derived fields on protected system
  # classes. The class is still returned (so callers that call
  # build! purely for the class lookup continue to work) but no
  # attacker-controlled belongs_to/has_many/property is added.
  if PROTECTED_SYSTEM_CLASSES.include?(className.to_s)
    return klass
  end

  base_fields = Parse::Properties::BASE.keys
  class_fields = klass.field_map.values + [:className]
  fields.each do |field, type|
    field = field.to_sym
    key = field.to_s.underscore.to_sym
    next if base_fields.include?(field) || class_fields.include?(field)

    data_type = type[:type].downcase.to_sym
    if data_type == :pointer
      klass.belongs_to key, as: safe_target_class(type[:targetClass]), field: field
    elsif data_type == :relation
      klass.has_many key, through: :relation, as: safe_target_class(type[:targetClass]), field: field
    else
      klass.property key, data_type, field: field
    end
    class_fields.push(field)
  end
  klass
end