parse-stack-next 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
require "active_support/core_ext/object"
|
|
8
|
+
require "active_support/core_ext"
|
|
9
|
+
require "active_support/security_utils"
|
|
10
|
+
require "active_model/serializers/json"
|
|
11
|
+
require "rack"
|
|
12
|
+
require "ostruct"
|
|
13
|
+
require_relative "client"
|
|
14
|
+
# Note: Do not require "stack" here - this file is loaded from stack.rb
|
|
15
|
+
# and adding that require would create a circular dependency.
|
|
16
|
+
require_relative "model/object"
|
|
17
|
+
require_relative "webhooks/payload"
|
|
18
|
+
require_relative "webhooks/registration"
|
|
19
|
+
require_relative "webhooks/replay_protection"
|
|
20
|
+
|
|
21
|
+
module Parse
|
|
22
|
+
class Object
|
|
23
|
+
|
|
24
|
+
# Register a webhook function for this subclass.
|
|
25
|
+
# @example
|
|
26
|
+
# class Post < Parse::Object
|
|
27
|
+
#
|
|
28
|
+
# webhook_function :helloWorld do
|
|
29
|
+
# # ... do something when this function is called ...
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# @param functionName [String] the literal name of the function to be registered with the server.
|
|
33
|
+
# @yield (see Parse::Object.webhook)
|
|
34
|
+
# @param block (see Parse::Object.webhook)
|
|
35
|
+
# @return (see Parse::Object.webhook)
|
|
36
|
+
def self.webhook_function(functionName, &block)
|
|
37
|
+
if block_given?
|
|
38
|
+
Parse::Webhooks.route(:function, functionName, &block)
|
|
39
|
+
else
|
|
40
|
+
block = functionName.to_s.underscore.to_sym if block.blank?
|
|
41
|
+
block = method(block.to_sym) if block.is_a?(Symbol)
|
|
42
|
+
Parse::Webhooks.route(:function, functionName, block)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Register a webhook trigger or function for this subclass.
|
|
47
|
+
# @example
|
|
48
|
+
# class Post < Parse::Object
|
|
49
|
+
#
|
|
50
|
+
# webhook :before_save do
|
|
51
|
+
# # ... do something ...
|
|
52
|
+
# parse_object
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# end
|
|
56
|
+
# @param type (see Parse::Webhooks.route)
|
|
57
|
+
# @yield the body of the function to be evaluated in the scope of a {Parse::Webhooks::Payload} instance.
|
|
58
|
+
# @param block [Symbol] the name of the method to call, if no block is passed.
|
|
59
|
+
# @return (see Parse::Webhooks.route)
|
|
60
|
+
def self.webhook(type, &block)
|
|
61
|
+
if type == :function
|
|
62
|
+
unless block.is_a?(String) || block.is_a?(Symbol)
|
|
63
|
+
raise ArgumentError, "Invalid Cloud Code function name: #{block}"
|
|
64
|
+
end
|
|
65
|
+
Parse::Webhooks.route(:function, block, &block)
|
|
66
|
+
# then block must be a symbol or a string
|
|
67
|
+
else
|
|
68
|
+
if block_given?
|
|
69
|
+
Parse::Webhooks.route(type, self, &block)
|
|
70
|
+
else
|
|
71
|
+
Parse::Webhooks.route(type, self, block)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
#if block
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A Rack-based application middlware to handle incoming Parse cloud code webhook
|
|
80
|
+
# requests.
|
|
81
|
+
class Webhooks
|
|
82
|
+
# The error to be raised in registered trigger or function webhook blocks that
|
|
83
|
+
# will trigger the Parse::Webhooks application to return the proper error response.
|
|
84
|
+
class ResponseError < StandardError; end
|
|
85
|
+
|
|
86
|
+
include Client::Connectable
|
|
87
|
+
extend Parse::Webhooks::Registration
|
|
88
|
+
# The name of the incoming env containing the webhook key.
|
|
89
|
+
HTTP_PARSE_WEBHOOK = "HTTP_X_PARSE_WEBHOOK_KEY"
|
|
90
|
+
# The name of the incoming env containing the application id key.
|
|
91
|
+
HTTP_PARSE_APPLICATION_ID = "HTTP_X_PARSE_APPLICATION_ID"
|
|
92
|
+
# The content type that needs to be sent back to Parse server.
|
|
93
|
+
CONTENT_TYPE = "application/json"
|
|
94
|
+
|
|
95
|
+
# The Parse Webhook Key to be used for authenticating webhook requests.
|
|
96
|
+
# See {Parse::Webhooks.key} on setting this value.
|
|
97
|
+
# @return [String]
|
|
98
|
+
def key
|
|
99
|
+
self.class.key
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class << self
|
|
103
|
+
|
|
104
|
+
# Allows support for web frameworks that support auto-reloading of source.
|
|
105
|
+
# @!visibility private
|
|
106
|
+
def reload!(args = {})
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @return [Boolean] whether to print additional logging information. You may also
|
|
110
|
+
# set this to `:debug` for additional verbosity.
|
|
111
|
+
attr_accessor :logging
|
|
112
|
+
|
|
113
|
+
# A hash-like structure composing of all the registered webhook
|
|
114
|
+
# triggers and functions. These are `:before_save`, `:after_save`,
|
|
115
|
+
# `:before_delete`, `:after_delete` or `:function`.
|
|
116
|
+
# @return [OpenStruct]
|
|
117
|
+
def routes
|
|
118
|
+
return @routes unless @routes.nil?
|
|
119
|
+
r = Parse::API::Hooks::TRIGGER_NAMES_LOCAL + [:function]
|
|
120
|
+
@routes = OpenStruct.new(r.reduce({}) { |h, t| h[t] = {}; h })
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Internally registers a route for a specific webhook trigger or function.
|
|
124
|
+
# @param type [Symbol] The type of cloud code webhook to register. This can be any
|
|
125
|
+
# of the supported routes. These are `:before_save`, `:after_save`,
|
|
126
|
+
# `:before_delete`, `:after_delete` or `:function`.
|
|
127
|
+
# @param className [String] if `type` is not `:function`, then this registers
|
|
128
|
+
# a trigger for the given className. Otherwise, className is treated to be the function
|
|
129
|
+
# name to register with Parse server.
|
|
130
|
+
# @yield the block that will handle of the webhook trigger or function.
|
|
131
|
+
# @return (see routes)
|
|
132
|
+
def route(type, className, &block)
|
|
133
|
+
type = type.to_s.underscore.to_sym #support camelcase
|
|
134
|
+
if type != :function && className.respond_to?(:parse_class)
|
|
135
|
+
className = className.parse_class
|
|
136
|
+
end
|
|
137
|
+
className = className.to_s
|
|
138
|
+
if routes[type].nil? || block.respond_to?(:call) == false
|
|
139
|
+
raise ArgumentError, "Invalid Webhook registration trigger #{type} #{className}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# AfterSave/AfterDelete hooks support more than one
|
|
143
|
+
if type == :after_save || type == :after_delete
|
|
144
|
+
routes[type][className] ||= []
|
|
145
|
+
routes[type][className].push block
|
|
146
|
+
else
|
|
147
|
+
routes[type][className] = block
|
|
148
|
+
end
|
|
149
|
+
@routes
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Run a locally registered webhook function. This bypasses calling a
|
|
153
|
+
# function through Parse-Server if the method handler is registered locally.
|
|
154
|
+
# @return [Object] the result of the function.
|
|
155
|
+
def run_function(name, params)
|
|
156
|
+
payload = Payload.new
|
|
157
|
+
payload.function_name = name
|
|
158
|
+
payload.params = params
|
|
159
|
+
call_route(:function, name, payload)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Calls the set of registered webhook trigger blocks or the specific function block.
|
|
163
|
+
# This method is usually called when an incoming request from Parse Server is received.
|
|
164
|
+
# @param type (see route)
|
|
165
|
+
# @param className (see route)
|
|
166
|
+
# @param payload [Parse::Webhooks::Payload] the payload object received from the server.
|
|
167
|
+
# @return [Object] the result of the trigger or function.
|
|
168
|
+
def call_route(type, className, payload = nil)
|
|
169
|
+
type = type.to_s.underscore.to_sym #support camelcase
|
|
170
|
+
className = className.parse_class if className.respond_to?(:parse_class)
|
|
171
|
+
className = className.to_s
|
|
172
|
+
|
|
173
|
+
return unless routes[type].present? && routes[type][className].present?
|
|
174
|
+
registry = routes[type][className]
|
|
175
|
+
|
|
176
|
+
# Track the header-derived ruby_initiated flag on the payload so
|
|
177
|
+
# user code can introspect it (`payload.ruby_initiated?`). For the
|
|
178
|
+
# framework's own callback-deduplication logic below we use the
|
|
179
|
+
# stricter `trusted_ruby_initiated`, which additionally requires the
|
|
180
|
+
# master key. The X-Parse-Request-Id header is client-controllable,
|
|
181
|
+
# so honoring `_RB_` alone would let any client send `_RB_attacker`
|
|
182
|
+
# and trick the framework into skipping server-side callbacks.
|
|
183
|
+
# Server-side Parse-Stack saves use the master key by default, so
|
|
184
|
+
# the AND is a safe condition for legitimate Ruby-initiated traffic.
|
|
185
|
+
if payload
|
|
186
|
+
request_id = payload&.raw&.dig(:headers, "x-parse-request-id") ||
|
|
187
|
+
payload&.raw&.dig("headers", "x-parse-request-id") ||
|
|
188
|
+
payload&.raw&.dig(:headers, "X-Parse-Request-Id") ||
|
|
189
|
+
payload&.raw&.dig("headers", "X-Parse-Request-Id")
|
|
190
|
+
ruby_initiated = request_id&.start_with?("_RB_") || false
|
|
191
|
+
payload.instance_variable_set(:@ruby_initiated, ruby_initiated)
|
|
192
|
+
trusted_ruby_initiated = ruby_initiated && (payload.master? == true)
|
|
193
|
+
else
|
|
194
|
+
ruby_initiated = false
|
|
195
|
+
trusted_ruby_initiated = false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Pre-block: apply declarative write protection (guard :field, :mode)
|
|
199
|
+
# to the parse_object that the handler will receive. Running BEFORE
|
|
200
|
+
# the handler block means trusted server-side writes performed inside
|
|
201
|
+
# the block are preserved -- only client-supplied values for guarded
|
|
202
|
+
# fields are reverted.
|
|
203
|
+
#
|
|
204
|
+
# Notably we do NOT gate this on ruby_initiated. That flag derives
|
|
205
|
+
# from a client-controlled X-Parse-Request-Id header, so trusting it
|
|
206
|
+
# to bypass write protection would allow a one-header attack. Master
|
|
207
|
+
# key requests still bypass via the master:/payload.master? check.
|
|
208
|
+
if type == :before_save && payload && payload.object?
|
|
209
|
+
klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
|
|
210
|
+
if klass && klass.respond_to?(:field_guards) && klass.field_guards.any?
|
|
211
|
+
pre_obj = payload.parse_object # memoized; the handler sees this same instance
|
|
212
|
+
if pre_obj.respond_to?(:apply_field_guards!)
|
|
213
|
+
pre_obj.apply_field_guards!(
|
|
214
|
+
master: payload.master? || false,
|
|
215
|
+
is_new: payload.original.blank?
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
if registry.is_a?(Array)
|
|
222
|
+
result = registry.map { |hook| payload.instance_exec(payload, &hook) }.last
|
|
223
|
+
else
|
|
224
|
+
result = payload.instance_exec(payload, ®istry)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if result.is_a?(Parse::Object)
|
|
228
|
+
# if it is a Parse::Object, we will call the registered ActiveModel callbacks
|
|
229
|
+
if type == :before_save
|
|
230
|
+
# returning false from the callback block only runs the before_* callback
|
|
231
|
+
# Skip prepare_save! when this request is trusted-Ruby-initiated
|
|
232
|
+
# (both `_RB_` header AND master key), since Parse-Stack already
|
|
233
|
+
# ran ActiveModel before_save callbacks locally. A client-spoofed
|
|
234
|
+
# `_RB_` without master falls through and runs them here.
|
|
235
|
+
unless trusted_ruby_initiated
|
|
236
|
+
prepare_result = result.prepare_save!
|
|
237
|
+
# If prepare_save! returns false (callback chain was halted), throw an error
|
|
238
|
+
if prepare_result == false
|
|
239
|
+
raise Parse::Webhooks::ResponseError, "Save halted by before_save callback"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
# For before_save, return the changes payload (what Parse Server expects)
|
|
243
|
+
result = result.changes_payload
|
|
244
|
+
elsif type == :before_delete
|
|
245
|
+
result.run_callbacks(:destroy) { false }
|
|
246
|
+
result = true
|
|
247
|
+
end
|
|
248
|
+
elsif type == :before_save && result == false
|
|
249
|
+
# If webhook block returns false, halt the save by throwing an error
|
|
250
|
+
raise Parse::Webhooks::ResponseError, "Save halted by before_save webhook"
|
|
251
|
+
elsif type == :before_save && (result == true || result.nil?)
|
|
252
|
+
# Open Source Parse server does not accept true results on before_save hooks.
|
|
253
|
+
result = {}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Guard-injection: when a handler returns a Hash (or true/nil normalized
|
|
257
|
+
# to {}) for a class with field_guards, Parse Server would otherwise
|
|
258
|
+
# merge the response with the client's original payload and persist
|
|
259
|
+
# the client-supplied values for guarded fields. Inject the pre-built
|
|
260
|
+
# parse_object's changes_payload entries for any guarded field so the
|
|
261
|
+
# response carries the appropriate revert (Delete op on create, prior
|
|
262
|
+
# value on update). The Parse::Object return path already runs through
|
|
263
|
+
# changes_payload on the same memoized instance and therefore needs no
|
|
264
|
+
# extra injection.
|
|
265
|
+
if type == :before_save && result.is_a?(Hash) && payload && payload.object?
|
|
266
|
+
guard_klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
|
|
267
|
+
if guard_klass && guard_klass.respond_to?(:field_guards) && guard_klass.field_guards.any?
|
|
268
|
+
pre_obj = payload.parse_object # same memoized instance the pre-block step mutated
|
|
269
|
+
if pre_obj.respond_to?(:changes_payload)
|
|
270
|
+
guard_payload = pre_obj.changes_payload
|
|
271
|
+
field_map = guard_klass.respond_to?(:field_map) ? guard_klass.field_map : {}
|
|
272
|
+
guard_klass.field_guards.each_key do |field|
|
|
273
|
+
remote = (field_map[field.to_sym] || field).to_s
|
|
274
|
+
result[remote] = guard_payload[remote] if guard_payload.key?(remote)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if type == :after_save && (result == true || result.nil?) && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object)
|
|
281
|
+
# Handle after_save callbacks intelligently based on request origin.
|
|
282
|
+
# For trusted-Ruby-initiated saves (both `_RB_` header AND master
|
|
283
|
+
# key), Parse Stack's local `run_callbacks :save` will fire
|
|
284
|
+
# after_create and after_save callbacks after the REST response
|
|
285
|
+
# returns; firing them again here would double-fire any side
|
|
286
|
+
# effect (e.g. an `after_save :send_email` would send two emails
|
|
287
|
+
# per save). For everything else -- client-initiated saves, or a
|
|
288
|
+
# spoofed `_RB_` from a non-master client -- Parse Stack never had
|
|
289
|
+
# a chance to run callbacks, so we fire them here.
|
|
290
|
+
is_new = payload.original.nil?
|
|
291
|
+
unless trusted_ruby_initiated
|
|
292
|
+
payload.parse_object.run_after_create_callbacks if is_new
|
|
293
|
+
payload.parse_object.run_after_save_callbacks
|
|
294
|
+
end
|
|
295
|
+
result = true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
result
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Generates a success response for Parse Server.
|
|
302
|
+
# @param data [Object] the data to send back with the success.
|
|
303
|
+
# @return [Hash] a success data payload
|
|
304
|
+
def success(data = true)
|
|
305
|
+
{ success: data }.to_json
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Generates an error response for Parse Server.
|
|
309
|
+
# @param data [Object] the data to send back with the error.
|
|
310
|
+
# @return [Hash] a error data payload
|
|
311
|
+
def error(data = false)
|
|
312
|
+
{ error: data }.to_json
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# @!attribute key
|
|
316
|
+
# Returns the configured webhook key if available. By default it will use
|
|
317
|
+
# the value of ENV['PARSE_SERVER_WEBHOOK_KEY'] if not configured.
|
|
318
|
+
# @return [String]
|
|
319
|
+
def key=(value)
|
|
320
|
+
@key = value
|
|
321
|
+
# Reset the warn-once flag so a deployment that configures the key
|
|
322
|
+
# after startup gets a clean state if the key is later cleared.
|
|
323
|
+
@missing_key_warned = nil
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def key
|
|
327
|
+
@key ||= ENV["PARSE_SERVER_WEBHOOK_KEY"] || ENV["PARSE_WEBHOOK_KEY"]
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# When no webhook key is configured, the endpoint refuses requests by
|
|
331
|
+
# default. Set this to true (or set PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED=true)
|
|
332
|
+
# to opt into the legacy permissive behavior for local development.
|
|
333
|
+
# @return [Boolean]
|
|
334
|
+
attr_writer :allow_unauthenticated
|
|
335
|
+
|
|
336
|
+
def allow_unauthenticated
|
|
337
|
+
return @allow_unauthenticated unless @allow_unauthenticated.nil?
|
|
338
|
+
ENV["PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED"] == "true"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# When set, {Parse::Webhooks::Registration#assert_webhook_url_safe!}
|
|
342
|
+
# skips the DNS resolution and private/internal CIDR refusal. Other
|
|
343
|
+
# checks (scheme, userinfo, host presence) still apply. Intended for
|
|
344
|
+
# integration tests that register webhooks at Docker bridge hosts
|
|
345
|
+
# (e.g. +host.docker.internal+) which only resolve from inside the
|
|
346
|
+
# Parse Server container. May also be enabled via
|
|
347
|
+
# +PARSE_WEBHOOK_ALLOW_PRIVATE_URLS=true+. Do not enable in
|
|
348
|
+
# production: the resolution guard is what blocks attacker-driven
|
|
349
|
+
# webhook redirection to internal hosts.
|
|
350
|
+
# @return [Boolean]
|
|
351
|
+
attr_writer :allow_private_webhook_urls
|
|
352
|
+
|
|
353
|
+
def allow_private_webhook_urls
|
|
354
|
+
return @allow_private_webhook_urls unless @allow_private_webhook_urls.nil?
|
|
355
|
+
ENV["PARSE_WEBHOOK_ALLOW_PRIVATE_URLS"] == "true"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Standard Rack call method. This method processes an incoming cloud code
|
|
359
|
+
# webhook request from Parse Server, validates it and executes any registered handlers for it.
|
|
360
|
+
# The result of the handler for the matching webhook request is sent back to
|
|
361
|
+
# Parse Server. If the handler raises a {Parse::Webhooks::ResponseError},
|
|
362
|
+
# it will return the proper error response.
|
|
363
|
+
# @raise Parse::Webhooks::ResponseError whenever {Parse::Object}, ActiveModel::ValidationError
|
|
364
|
+
# @param env [Hash] the environment hash in a Rack request.
|
|
365
|
+
# @return [Array] the value of calling `finish` on the {http://www.rubydoc.info/github/rack/rack/Rack/Response Rack::Response} object.
|
|
366
|
+
def call(env)
|
|
367
|
+
# Thraed safety
|
|
368
|
+
dup.call!(env)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# @!visibility private
|
|
372
|
+
def call!(env)
|
|
373
|
+
request = Rack::Request.new env
|
|
374
|
+
response = Rack::Response.new
|
|
375
|
+
|
|
376
|
+
if self.key.present?
|
|
377
|
+
provided_key = request.env[HTTP_PARSE_WEBHOOK].to_s
|
|
378
|
+
unless ActiveSupport::SecurityUtils.secure_compare(self.key, provided_key)
|
|
379
|
+
puts "[Parse::Webhooks] Invalid Parse-Webhook Key received"
|
|
380
|
+
response.write error("Invalid Parse Webhook Key")
|
|
381
|
+
return response.finish
|
|
382
|
+
end
|
|
383
|
+
elsif !self.allow_unauthenticated
|
|
384
|
+
# Fail closed: without a configured webhook key, any host on the
|
|
385
|
+
# network could fire authenticated cloud triggers. Set
|
|
386
|
+
# PARSE_SERVER_WEBHOOK_KEY (matching the Parse Server config) or
|
|
387
|
+
# opt in to permissive mode via PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED=true.
|
|
388
|
+
# Log the warning only once; otherwise an attacker hammering the
|
|
389
|
+
# endpoint can fill disk with repeated warnings. The flag lives on
|
|
390
|
+
# the original Parse::Webhooks class (not the per-request dup created
|
|
391
|
+
# by `call`), so it persists across requests.
|
|
392
|
+
unless Parse::Webhooks.instance_variable_get(:@missing_key_warned)
|
|
393
|
+
Parse::Webhooks.instance_variable_set(:@missing_key_warned, true)
|
|
394
|
+
warn "[Parse::Webhooks] Refusing requests: no webhook key configured. " \
|
|
395
|
+
"Set PARSE_SERVER_WEBHOOK_KEY or Parse::Webhooks.allow_unauthenticated = true."
|
|
396
|
+
end
|
|
397
|
+
response.write error("Webhook key not configured.")
|
|
398
|
+
return response.finish
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Use Rack's media_type (strips parameters/whitespace and lowercases)
|
|
402
|
+
# so the comparison is exact. The previous substring check on the raw
|
|
403
|
+
# Content-Type header accepted look-alikes like "application/jsonp"
|
|
404
|
+
# or "text/application/json" that should be rejected.
|
|
405
|
+
unless request.media_type == CONTENT_TYPE
|
|
406
|
+
response.write error("Invalid content-type format. Should be application/json.")
|
|
407
|
+
return response.finish
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
request.body.rewind
|
|
411
|
+
body_str = request.body.read
|
|
412
|
+
if body_str.bytesize > 1_048_576
|
|
413
|
+
response.write error("Payload too large.")
|
|
414
|
+
return response.finish
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# NEW-EXT-4: reject in-window replays and (when configured)
|
|
418
|
+
# require a fresh HMAC over the body. Done before JSON parsing so
|
|
419
|
+
# a malformed payload can't bypass dedup, and before any handler
|
|
420
|
+
# runs so side effects aren't repeated.
|
|
421
|
+
replay_error = ReplayProtection.verify!(
|
|
422
|
+
request.env,
|
|
423
|
+
body_str,
|
|
424
|
+
request.env["HTTP_X_PARSE_REQUEST_ID"]
|
|
425
|
+
)
|
|
426
|
+
if replay_error
|
|
427
|
+
response.write error(replay_error)
|
|
428
|
+
return response.finish
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
begin
|
|
432
|
+
payload = Parse::Webhooks::Payload.new body_str
|
|
433
|
+
rescue => e
|
|
434
|
+
warn "Invalid webhook payload format: #{e}"
|
|
435
|
+
response.write error("Invalid payload format. Should be valid JSON.")
|
|
436
|
+
return response.finish
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
if self.logging.present?
|
|
440
|
+
if payload.trigger?
|
|
441
|
+
puts "[Webhooks::Request] --> #{payload.trigger_name} #{payload.parse_class}:#{payload.parse_id}"
|
|
442
|
+
elsif payload.function?
|
|
443
|
+
puts "[ParseWebhooks Request] --> Function #{payload.function_name}"
|
|
444
|
+
end
|
|
445
|
+
if self.logging == :debug
|
|
446
|
+
puts "[Webhooks::Payload] ----------------------------"
|
|
447
|
+
puts Parse::Middleware::BodyBuilder.redact(payload.as_json.to_json)
|
|
448
|
+
puts "----------------------------------------------------\n"
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
begin
|
|
453
|
+
result = true
|
|
454
|
+
if payload.function? && payload.function_name.present?
|
|
455
|
+
result = Parse::Webhooks.call_route(:function, payload.function_name, payload)
|
|
456
|
+
elsif payload.trigger? && payload.parse_class.present? && payload.trigger_name.present?
|
|
457
|
+
# call hooks subscribed to the specific class
|
|
458
|
+
result = Parse::Webhooks.call_route(payload.trigger_name, payload.parse_class, payload)
|
|
459
|
+
|
|
460
|
+
# call hooks subscribed to any class route
|
|
461
|
+
generic_result = Parse::Webhooks.call_route(payload.trigger_name, "*", payload)
|
|
462
|
+
result = generic_result if generic_result.present? && result.nil?
|
|
463
|
+
else
|
|
464
|
+
if self.logging.present?
|
|
465
|
+
puts "[Webhooks] --> Could not find mapping route for " \
|
|
466
|
+
"#{Parse::Middleware::BodyBuilder.redact(payload.to_json)}"
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
result = true if result.nil?
|
|
471
|
+
if self.logging.present?
|
|
472
|
+
puts "[Webhooks::Response] ----------------------------"
|
|
473
|
+
puts success(result)
|
|
474
|
+
puts "----------------------------------------------------\n"
|
|
475
|
+
end
|
|
476
|
+
response.write success(result)
|
|
477
|
+
return response.finish
|
|
478
|
+
rescue Parse::Webhooks::ResponseError, ActiveModel::ValidationError => e
|
|
479
|
+
if payload.trigger?
|
|
480
|
+
puts "[Webhooks::ResponseError] >> #{payload.trigger_name} #{payload.parse_class}:#{payload.parse_id}: #{e}"
|
|
481
|
+
elsif payload.function?
|
|
482
|
+
puts "[Webhooks::ResponseError] >> #{payload.function_name}: #{e}"
|
|
483
|
+
end
|
|
484
|
+
response.write error(e.to_s)
|
|
485
|
+
return response.finish
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
#check if we can handle the type trigger/functionName
|
|
489
|
+
response.write(success)
|
|
490
|
+
response.finish
|
|
491
|
+
end # call
|
|
492
|
+
end #class << self
|
|
493
|
+
end # Webhooks
|
|
494
|
+
end # Parse
|
|
495
|
+
|
|
496
|
+
# Load-order fixup for {Parse::Core::FieldGuards}: classes that declared
|
|
497
|
+
# `guard` in their class body (e.g. {Parse::User}) ran before this file
|
|
498
|
+
# was required, so their `ensure_field_guards_webhook!` call short-circuited
|
|
499
|
+
# with a "Parse::Webhooks not yet defined" guard. Walk every Parse::Object
|
|
500
|
+
# subclass that ended up with a non-empty `field_guards` hash and register
|
|
501
|
+
# the stub route now that {Parse::Webhooks} exists. Application code that
|
|
502
|
+
# uses `guard` from its own model files (which are required after this
|
|
503
|
+
# file) hits the normal path and bypasses this fixup.
|
|
504
|
+
if defined?(Parse::Object) && Parse::Object.respond_to?(:descendants)
|
|
505
|
+
Parse::Object.descendants.each do |klass|
|
|
506
|
+
next unless klass.respond_to?(:field_guards) && klass.field_guards.any?
|
|
507
|
+
next unless klass.respond_to?(:ensure_field_guards_webhook!)
|
|
508
|
+
klass.ensure_field_guards_webhook!
|
|
509
|
+
end
|
|
510
|
+
end
|
data/lib/parse-stack.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "parse/stack/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "parse-stack-next"
|
|
8
|
+
spec.version = Parse::Stack::VERSION
|
|
9
|
+
spec.authors = ["Anthony Persaud", "Henry Spindell", "Adrian Curtin"]
|
|
10
|
+
spec.email = ["adrian+parse-stack@neurosynq.net"]
|
|
11
|
+
|
|
12
|
+
spec.summary = %q{Parse Server Ruby Client SDK (parse-stack-next fork)}
|
|
13
|
+
spec.description = %q{Parse Server Ruby Client. Perform Object-relational mapping between Parse Server and Ruby classes, with authentication, cloud code webhooks, push notifications and more built in.}
|
|
14
|
+
spec.homepage = "https://github.com/neurosynq/parse-stack-next"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
|
|
17
|
+
spec.metadata = {
|
|
18
|
+
"homepage_uri" => "https://github.com/neurosynq/parse-stack-next",
|
|
19
|
+
"source_code_uri" => "https://github.com/neurosynq/parse-stack-next",
|
|
20
|
+
"changelog_uri" => "https://github.com/neurosynq/parse-stack-next/blob/main/CHANGELOG.md",
|
|
21
|
+
"bug_tracker_uri" => "https://github.com/neurosynq/parse-stack-next/issues",
|
|
22
|
+
"documentation_uri" => "https://neurosynq.github.io/parse-stack-next/",
|
|
23
|
+
"rubygems_mfa_required" => "true",
|
|
24
|
+
}
|
|
25
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
|
26
|
+
# delete this section to allow pushing this gem to any host.
|
|
27
|
+
# if spec.respond_to?(:metadata)
|
|
28
|
+
# spec.metadata['allowed_push_host'] = "http://www.modernistik.com"
|
|
29
|
+
# else
|
|
30
|
+
# raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
|
31
|
+
# end
|
|
32
|
+
|
|
33
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
34
|
+
spec.bindir = "bin"
|
|
35
|
+
spec.executables = ["parse-console"] #spec.files.grep(%r{^bin/pstack/}) { |f| File.basename(f) }
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
spec.required_ruby_version = ">= 3.2"
|
|
38
|
+
|
|
39
|
+
spec.add_runtime_dependency "activemodel", [">= 6.1", "< 9"]
|
|
40
|
+
spec.add_runtime_dependency "activesupport", [">= 6.1", "< 9"]
|
|
41
|
+
spec.add_runtime_dependency "parallel", [">= 1.6", "< 3"]
|
|
42
|
+
spec.add_runtime_dependency "faraday", "~> 2.0"
|
|
43
|
+
spec.add_runtime_dependency "faraday-net_http_persistent", "~> 2.0"
|
|
44
|
+
spec.add_runtime_dependency "moneta", "< 2"
|
|
45
|
+
spec.add_runtime_dependency "rack", ">= 2.0.6", "< 4"
|
|
46
|
+
spec.add_runtime_dependency "csv", "~> 3.3"
|
|
47
|
+
spec.add_runtime_dependency "ostruct", "~> 0.6"
|
|
48
|
+
|
|
49
|
+
# Optional dependencies for MFA (Multi-Factor Authentication) support
|
|
50
|
+
# Users must add these to their Gemfile to use MFA features:
|
|
51
|
+
# gem 'rotp' # For TOTP generation/verification
|
|
52
|
+
# gem 'rqrcode' # For QR code generation
|
|
53
|
+
|
|
54
|
+
# Optional dependency for enhanced phone number validation
|
|
55
|
+
# Users can add this to their Gemfile for comprehensive phone validation:
|
|
56
|
+
# gem 'phonelib' # For full ITU-T E.164 validation with libphonenumber data
|
|
57
|
+
|
|
58
|
+
# Optional dependency for direct MongoDB queries and Atlas Search
|
|
59
|
+
# Required for: Parse::MongoDB, Parse::AtlasSearch, mongo_direct query methods
|
|
60
|
+
# Users can add this to their Gemfile for direct MongoDB access:
|
|
61
|
+
# gem 'mongo', '~> 2.18'
|
|
62
|
+
# Note: The gem is loaded at runtime only when MongoDB features are used
|
|
63
|
+
|
|
64
|
+
# spec.post_install_message = <<UPGRADE
|
|
65
|
+
#
|
|
66
|
+
# ** BREAKING CHANGES **
|
|
67
|
+
# The default `has_many` association form has changed from :array to :query.
|
|
68
|
+
# To use arrays, you must now pass `through: :array` option to `has_many`.
|
|
69
|
+
#
|
|
70
|
+
# Visit: https://github.com/modernistik/parse-stack/wiki/Changes-to-has_many-in-1.5.0
|
|
71
|
+
#
|
|
72
|
+
# UPGRADE
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
# After checking out the repo, run `bin/setup` to install dependencies. You can
|
|
77
|
+
# also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
78
|
+
#
|
|
79
|
+
# To install this gem onto your local machine, run `bundle exec rake install`.
|
|
80
|
+
# To release a new version, update the version number in `version.rb`, and then run
|
|
81
|
+
# `bundle exec rake release`, which will create a git tag for the version,
|
|
82
|
+
# push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
data/parse-stack.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Debug script to check Parse Server IP configuration
|
|
4
|
+
console.log('=== Parse Server IP Debug ===');
|
|
5
|
+
console.log('Environment variables:');
|
|
6
|
+
console.log('PARSE_SERVER_MASTER_KEY_IPS:', process.env.PARSE_SERVER_MASTER_KEY_IPS);
|
|
7
|
+
|
|
8
|
+
// Try to load the config file
|
|
9
|
+
try {
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const config = JSON.parse(fs.readFileSync('/parse-server/config/parse-config.json', 'utf8'));
|
|
12
|
+
console.log('\nConfig file masterKeyIps:', config.masterKeyIps);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
console.log('\nConfig file error:', e.message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check what Parse Server would use as default
|
|
18
|
+
const defaultIps = ['127.0.0.1', '::1'];
|
|
19
|
+
console.log('\nDefault masterKeyIps:', defaultIps);
|
|
20
|
+
|
|
21
|
+
// Test IP parsing
|
|
22
|
+
const envIps = process.env.PARSE_SERVER_MASTER_KEY_IPS;
|
|
23
|
+
if (envIps) {
|
|
24
|
+
const parsedIps = envIps.split(',');
|
|
25
|
+
console.log('\nParsed environment IPs:', parsedIps);
|
|
26
|
+
|
|
27
|
+
const net = require('net');
|
|
28
|
+
parsedIps.forEach(ip => {
|
|
29
|
+
const cleanIp = ip.includes('/') ? ip.split('/')[0] : ip;
|
|
30
|
+
console.log(`IP "${ip}" -> clean: "${cleanIp}" -> isIP: ${net.isIP(cleanIp)}`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log('\nRequest IP that Parse Server sees: (this would be logged in Parse Server)');
|
|
35
|
+
console.log('Expected request IP: 172.18.0.1 (Docker container network)');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
FROM parseplatform/parse-server:9
|
|
2
|
+
|
|
3
|
+
# Switch to root to copy and set permissions
|
|
4
|
+
USER root
|
|
5
|
+
|
|
6
|
+
# Copy our custom startup script with execute permissions
|
|
7
|
+
COPY --chmod=755 start-parse.sh /start-parse.sh
|
|
8
|
+
|
|
9
|
+
# Switch back to node user (if needed)
|
|
10
|
+
USER node
|
|
11
|
+
|
|
12
|
+
# Set the entrypoint to our script
|
|
13
|
+
ENTRYPOINT ["/bin/sh", "/start-parse.sh"]
|