55
66module Hooks
77 module Core
8- # Loads and caches all plugins (auth + handlers) at boot time
8+ # Loads and caches all plugins (auth + handlers + lifecycle ) at boot time
99 class PluginLoader
1010 # Class-level registries for loaded plugins
1111 @auth_plugins = { }
1212 @handler_plugins = { }
13+ @lifecycle_plugins = [ ]
1314
1415 class << self
15- attr_reader :auth_plugins , :handler_plugins
16+ attr_reader :auth_plugins , :handler_plugins , :lifecycle_plugins
1617
1718 # Load all plugins at boot time
1819 #
@@ -22,13 +23,15 @@ def load_all_plugins(config)
2223 # Clear existing registries
2324 @auth_plugins = { }
2425 @handler_plugins = { }
26+ @lifecycle_plugins = [ ]
2527
2628 # Load built-in plugins first
2729 load_builtin_plugins
2830
2931 # Load custom plugins if directories are configured
3032 load_custom_auth_plugins ( config [ :auth_plugin_dir ] ) if config [ :auth_plugin_dir ]
3133 load_custom_handler_plugins ( config [ :handler_plugin_dir ] ) if config [ :handler_plugin_dir ]
34+ load_custom_lifecycle_plugins ( config [ :lifecycle_plugin_dir ] ) if config [ :lifecycle_plugin_dir ]
3235
3336 # Log loaded plugins
3437 log_loaded_plugins
@@ -71,6 +74,7 @@ def get_handler_plugin(handler_name)
7174 def clear_plugins
7275 @auth_plugins = { }
7376 @handler_plugins = { }
77+ @lifecycle_plugins = [ ]
7478 end
7579
7680 private
@@ -119,6 +123,22 @@ def load_custom_handler_plugins(handler_plugin_dir)
119123 end
120124 end
121125
126+ # Load custom lifecycle plugins from directory
127+ #
128+ # @param lifecycle_plugin_dir [String] Directory containing custom lifecycle plugins
129+ # @return [void]
130+ def load_custom_lifecycle_plugins ( lifecycle_plugin_dir )
131+ return unless lifecycle_plugin_dir && Dir . exist? ( lifecycle_plugin_dir )
132+
133+ Dir . glob ( File . join ( lifecycle_plugin_dir , "*.rb" ) ) . sort . each do |file_path |
134+ begin
135+ load_custom_lifecycle_plugin ( file_path , lifecycle_plugin_dir )
136+ rescue => e
137+ raise StandardError , "Failed to load lifecycle plugin from #{ file_path } : #{ e . message } "
138+ end
139+ end
140+ end
141+
122142 # Load a single custom auth plugin file
123143 #
124144 # @param file_path [String] Path to the auth plugin file
@@ -189,6 +209,41 @@ def load_custom_handler_plugin(file_path, handler_plugin_dir)
189209 @handler_plugins [ class_name ] = handler_class
190210 end
191211
212+ # Load a single custom lifecycle plugin file
213+ #
214+ # @param file_path [String] Path to the lifecycle plugin file
215+ # @param lifecycle_plugin_dir [String] Base directory for lifecycle plugins
216+ # @return [void]
217+ def load_custom_lifecycle_plugin ( file_path , lifecycle_plugin_dir )
218+ # Security: Ensure the file path doesn't escape the lifecycle plugin directory
219+ normalized_lifecycle_dir = Pathname . new ( File . expand_path ( lifecycle_plugin_dir ) )
220+ normalized_file_path = Pathname . new ( File . expand_path ( file_path ) )
221+ unless normalized_file_path . descend . any? { |path | path == normalized_lifecycle_dir }
222+ raise SecurityError , "Lifecycle plugin path outside of lifecycle plugin directory: #{ file_path } "
223+ end
224+
225+ # Extract class name from file (e.g., logging_lifecycle.rb -> LoggingLifecycle)
226+ file_name = File . basename ( file_path , ".rb" )
227+ class_name = file_name . split ( "_" ) . map ( &:capitalize ) . join ( "" )
228+
229+ # Security: Validate class name
230+ unless valid_lifecycle_class_name? ( class_name )
231+ raise StandardError , "Invalid lifecycle plugin class name: #{ class_name } "
232+ end
233+
234+ # Load the file
235+ require file_path
236+
237+ # Get the class and validate it
238+ lifecycle_class = Object . const_get ( class_name )
239+ unless lifecycle_class < Hooks ::Plugins ::Lifecycle
240+ raise StandardError , "Lifecycle plugin class must inherit from Hooks::Plugins::Lifecycle: #{ class_name } "
241+ end
242+
243+ # Register the plugin instance
244+ @lifecycle_plugins << lifecycle_class . new
245+ end
246+
192247 # Log summary of loaded plugins
193248 #
194249 # @return [void]
@@ -201,6 +256,7 @@ def log_loaded_plugins
201256
202257 log . info "Loaded #{ @auth_plugins . size } auth plugins: #{ @auth_plugins . keys . join ( ', ' ) } "
203258 log . info "Loaded #{ @handler_plugins . size } handler plugins: #{ @handler_plugins . keys . join ( ', ' ) } "
259+ log . info "Loaded #{ @lifecycle_plugins . size } lifecycle plugins"
204260 end
205261
206262 # Validate that an auth plugin class name is safe to load
@@ -244,6 +300,27 @@ def valid_handler_class_name?(class_name)
244300
245301 true
246302 end
303+
304+ # Validate that a lifecycle plugin class name is safe to load
305+ #
306+ # @param class_name [String] The class name to validate
307+ # @return [Boolean] true if the class name is safe, false otherwise
308+ def valid_lifecycle_class_name? ( class_name )
309+ # Must be a string
310+ return false unless class_name . is_a? ( String )
311+
312+ # Must not be empty or only whitespace
313+ return false if class_name . strip . empty?
314+
315+ # Must match a safe pattern: alphanumeric + underscore, starting with uppercase
316+ # Examples: LoggingLifecycle, MetricsLifecycle, CustomLifecycle
317+ return false unless class_name . match? ( /\A [A-Z][a-zA-Z0-9_]*\z / )
318+
319+ # Must not be a system/built-in class name
320+ return false if Hooks ::Security ::DANGEROUS_CLASSES . include? ( class_name )
321+
322+ true
323+ end
247324 end
248325 end
249326 end
0 commit comments