Skip to content

Commit 8aef5bc

Browse files
CopilotGrantBirki
andcommitted
Implement boot-time plugin loading with PluginLoader class
Co-authored-by: GrantBirki <[email protected]>
1 parent 863c7f5 commit 8aef5bc

File tree

9 files changed

+454
-98
lines changed

9 files changed

+454
-98
lines changed

lib/hooks/app/auth/auth.rb

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative "../../core/plugin_loader"
4+
35
module Hooks
46
module App
57
# Provides authentication helpers for verifying incoming requests.
@@ -13,7 +15,7 @@ module Auth
1315
# @param payload [String, Hash] The request payload to authenticate.
1416
# @param headers [Hash] The request headers.
1517
# @param endpoint_config [Hash] The endpoint configuration, must include :auth key.
16-
# @param global_config [Hash] The global configuration (optional, needed for custom auth plugins).
18+
# @param global_config [Hash] The global configuration (optional, for compatibility).
1719
# @raise [StandardError] Raises error if authentication fails or is misconfigured.
1820
# @return [void]
1921
# @note This method will halt execution with an error if authentication fails.
@@ -26,34 +28,11 @@ def validate_auth!(payload, headers, endpoint_config, global_config = {})
2628
error!("authentication configuration missing or invalid", 500)
2729
end
2830

29-
auth_plugin_type = auth_type.downcase
30-
31-
auth_class = nil
32-
33-
case auth_plugin_type
34-
when "hmac"
35-
auth_class = Plugins::Auth::HMAC
36-
when "shared_secret"
37-
auth_class = Plugins::Auth::SharedSecret
38-
else
39-
# Try to load custom auth plugin if auth_plugin_dir is configured
40-
if global_config[:auth_plugin_dir]
41-
# Convert auth_type to CamelCase class name
42-
auth_plugin_class_name = auth_type.split("_").map(&:capitalize).join("")
43-
44-
# Validate the converted class name before attempting to load
45-
unless valid_auth_plugin_class_name?(auth_plugin_class_name)
46-
error!("invalid auth plugin type '#{auth_type}'", 400)
47-
end
48-
49-
begin
50-
auth_class = load_auth_plugin(auth_plugin_class_name, global_config[:auth_plugin_dir])
51-
rescue => e
52-
error!("failed to load custom auth plugin '#{auth_type}': #{e.message}", 500)
53-
end
54-
else
55-
error!("unsupported auth type '#{auth_type}' due to auth_plugin_dir not being set", 400)
56-
end
31+
# Get auth plugin from loaded plugins registry
32+
begin
33+
auth_class = Core::PluginLoader.get_auth_plugin(auth_type)
34+
rescue => e
35+
error!("unsupported auth type '#{auth_type}'", 400)
5736
end
5837

5938
unless auth_class.valid?(

lib/hooks/app/helpers.rb

Lines changed: 13 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "securerandom"
44
require_relative "../security"
5+
require_relative "../core/plugin_loader"
56

67
module Hooks
78
module App
@@ -64,85 +65,28 @@ def parse_payload(raw_body, headers, symbolize: true)
6465
# Load handler class
6566
#
6667
# @param handler_class_name [String] The name of the handler class to load
67-
# @param handler_dir [String] The directory containing handler files
68+
# @param handler_dir [String] The directory containing handler files (kept for compatibility)
6869
# @return [Object] An instance of the loaded handler class
69-
# @raise [LoadError] If the handler file or class cannot be found
70-
# @raise [StandardError] Halts with error if handler cannot be loaded
71-
def load_handler(handler_class_name, handler_dir)
72-
# Security: Validate handler class name to prevent arbitrary class loading
73-
unless valid_handler_class_name?(handler_class_name)
74-
error!("invalid handler class name: #{handler_class_name}", 400)
75-
end
76-
77-
# Convert class name to file name (e.g., Team1Handler -> team1_handler.rb)
78-
# E.g.2: GithubHandler -> github_handler.rb
79-
# E.g.3: GitHubHandler -> git_hub_handler.rb
80-
file_name = handler_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + ".rb"
81-
file_path = File.join(handler_dir, file_name)
82-
83-
# Security: Ensure the file path doesn't escape the handler directory
84-
normalized_handler_dir = Pathname.new(File.expand_path(handler_dir))
85-
normalized_file_path = Pathname.new(File.expand_path(file_path))
86-
unless normalized_file_path.descend.any? { |path| path == normalized_handler_dir }
87-
error!("handler path outside of handler directory", 400)
88-
end
89-
90-
if File.exist?(file_path)
91-
require file_path
92-
handler_class = Object.const_get(handler_class_name)
93-
94-
# Security: Ensure the loaded class inherits from the expected base class
95-
unless handler_class < Hooks::Plugins::Handlers::Base
96-
error!("handler class must inherit from Hooks::Plugins::Handlers::Base", 400)
97-
end
98-
70+
# @raise [StandardError] If handler cannot be found
71+
def load_handler(handler_class_name, handler_dir = nil)
72+
# Get handler class from loaded plugins registry
73+
begin
74+
handler_class = Core::PluginLoader.get_handler_plugin(handler_class_name)
9975
handler_class.new
100-
else
101-
raise LoadError, "Handler #{handler_class_name} not found at #{file_path}"
76+
rescue => e
77+
error!("failed to get handler '#{handler_class_name}': #{e.message}", 500)
10278
end
103-
rescue => e
104-
error!("failed to load handler: #{e.message}", 500)
10579
end
10680

107-
# Load auth plugin class
81+
# Load auth plugin class (DEPRECATED - plugins are now loaded at boot time)
10882
#
83+
# @deprecated This method is kept for compatibility but auth plugins are now loaded at boot time
10984
# @param auth_plugin_class_name [String] The name of the auth plugin class to load
11085
# @param auth_plugin_dir [String] The directory containing auth plugin files
11186
# @return [Class] The loaded auth plugin class
112-
# @raise [LoadError] If the auth plugin file or class cannot be found
113-
# @raise [StandardError] Halts with error if auth plugin cannot be loaded
87+
# @raise [StandardError] Always raises error as dynamic loading is no longer supported
11488
def load_auth_plugin(auth_plugin_class_name, auth_plugin_dir)
115-
# Security: Validate auth plugin class name to prevent arbitrary class loading
116-
unless valid_auth_plugin_class_name?(auth_plugin_class_name)
117-
error!("invalid auth plugin class name: #{auth_plugin_class_name}", 400)
118-
end
119-
120-
# Convert class name to file name (e.g., SomeCoolAuthPlugin -> some_cool_auth_plugin.rb)
121-
file_name = auth_plugin_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + ".rb"
122-
file_path = File.join(auth_plugin_dir, file_name)
123-
124-
# Security: Ensure the file path doesn't escape the auth plugin directory
125-
normalized_auth_plugin_dir = Pathname.new(File.expand_path(auth_plugin_dir))
126-
normalized_file_path = Pathname.new(File.expand_path(file_path))
127-
unless normalized_file_path.descend.any? { |path| path == normalized_auth_plugin_dir }
128-
error!("auth plugin path outside of auth plugin directory", 400)
129-
end
130-
131-
if File.exist?(file_path)
132-
require file_path
133-
auth_plugin_class = Object.const_get("Hooks::Plugins::Auth::#{auth_plugin_class_name}")
134-
135-
# Security: Ensure the loaded class inherits from the expected base class
136-
unless auth_plugin_class < Hooks::Plugins::Auth::Base
137-
error!("auth plugin class must inherit from Hooks::Plugins::Auth::Base", 400)
138-
end
139-
140-
auth_plugin_class
141-
else
142-
error!("Auth plugin #{auth_plugin_class_name} not found at #{file_path}", 500)
143-
end
144-
rescue => e
145-
error!("failed to load auth plugin: #{e.message}", 500)
89+
error!("Dynamic auth plugin loading is deprecated. Auth plugins are now loaded at boot time.", 500)
14690
end
14791

14892
private

lib/hooks/core/builder.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require_relative "config_loader"
44
require_relative "config_validator"
55
require_relative "logger_factory"
6+
require_relative "plugin_loader"
67
require_relative "../app/api"
78

89
module Hooks
@@ -33,6 +34,9 @@ def build
3334
)
3435
end
3536

37+
# Load all plugins at boot time
38+
load_plugins(config)
39+
3640
# Load endpoints
3741
endpoints = load_endpoints(config)
3842

@@ -75,6 +79,16 @@ def load_endpoints(config)
7579
rescue ConfigValidator::ValidationError => e
7680
raise ConfigurationError, "Endpoint validation failed: #{e.message}"
7781
end
82+
83+
# Load all plugins at boot time
84+
#
85+
# @param config [Hash] Global configuration
86+
# @return [void]
87+
def load_plugins(config)
88+
PluginLoader.load_all_plugins(config)
89+
rescue => e
90+
raise ConfigurationError, "Plugin loading failed: #{e.message}"
91+
end
7892
end
7993

8094
# Configuration error

0 commit comments

Comments
 (0)