Skip to content

Commit cc2bfcb

Browse files
CopilotGrantBirki
andcommitted
Remove all dynamic plugin loading for fully static boot-time only approach
Co-authored-by: GrantBirki <[email protected]>
1 parent c78fa68 commit cc2bfcb

File tree

5 files changed

+69
-528
lines changed

5 files changed

+69
-528
lines changed

lib/hooks/app/auth/auth.rb

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22

3-
require "pathname"
43
require_relative "../../core/plugin_loader"
54

65
module Hooks
@@ -29,27 +28,11 @@ def validate_auth!(payload, headers, endpoint_config, global_config = {})
2928
error!("authentication configuration missing or invalid", 500)
3029
end
3130

32-
# Get auth plugin from loaded plugins registry first
31+
# Get auth plugin from loaded plugins registry (boot-time loaded only)
3332
begin
3433
auth_class = Core::PluginLoader.get_auth_plugin(auth_type)
3534
rescue => e
36-
# If not found in registry and auth_plugin_dir is provided, fall back to dynamic loading
37-
if global_config[:auth_plugin_dir]
38-
begin
39-
auth_class = load_auth_plugin_dynamically(auth_type, global_config[:auth_plugin_dir])
40-
rescue => fallback_error
41-
# Preserve specific error messages for better debugging
42-
if fallback_error.message.include?("not found") ||
43-
fallback_error.message.include?("invalid auth plugin") ||
44-
fallback_error.message.include?("must inherit from")
45-
error!(fallback_error.message, 500)
46-
else
47-
error!("unsupported auth type '#{auth_type}'", 400)
48-
end
49-
end
50-
else
51-
error!("unsupported auth type '#{auth_type}'", 400)
52-
end
35+
error!("unsupported auth type '#{auth_type}'", 400)
5336
end
5437

5538
unless auth_class.valid?(
@@ -60,70 +43,6 @@ def validate_auth!(payload, headers, endpoint_config, global_config = {})
6043
error!("authentication failed", 401)
6144
end
6245
end
63-
64-
private
65-
66-
# Load auth plugin class dynamically (fallback for backward compatibility)
67-
#
68-
# @param auth_type [String] The auth type/plugin name
69-
# @param auth_plugin_dir [String] The directory containing auth plugin files
70-
# @return [Class] The loaded auth plugin class
71-
# @raise [StandardError] If the auth plugin cannot be loaded
72-
def load_auth_plugin_dynamically(auth_type, auth_plugin_dir)
73-
# Convert auth_type to CamelCase class name
74-
auth_plugin_class_name = auth_type.split("_").map(&:capitalize).join("")
75-
76-
# Validate the converted class name before attempting to load
77-
unless valid_auth_plugin_class_name?(auth_plugin_class_name)
78-
raise StandardError, "invalid auth plugin type '#{auth_type}'"
79-
end
80-
81-
# Convert class name to file name (e.g., SomeCoolAuthPlugin -> some_cool_auth_plugin.rb)
82-
file_name = auth_plugin_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + ".rb"
83-
file_path = File.join(auth_plugin_dir, file_name)
84-
85-
# Security: Ensure the file path doesn't escape the auth plugin directory
86-
normalized_auth_plugin_dir = Pathname.new(File.expand_path(auth_plugin_dir))
87-
normalized_file_path = Pathname.new(File.expand_path(file_path))
88-
unless normalized_file_path.descend.any? { |path| path == normalized_auth_plugin_dir }
89-
raise StandardError, "auth plugin path outside of auth plugin directory"
90-
end
91-
92-
if File.exist?(file_path)
93-
require file_path
94-
auth_plugin_class = Object.const_get("Hooks::Plugins::Auth::#{auth_plugin_class_name}")
95-
96-
# Security: Ensure the loaded class inherits from the expected base class
97-
unless auth_plugin_class < Hooks::Plugins::Auth::Base
98-
raise StandardError, "auth plugin class must inherit from Hooks::Plugins::Auth::Base"
99-
end
100-
101-
auth_plugin_class
102-
else
103-
raise StandardError, "Auth plugin #{auth_plugin_class_name} not found at #{file_path}"
104-
end
105-
end
106-
107-
# Validate that an auth plugin class name is safe to load
108-
#
109-
# @param class_name [String] The class name to validate
110-
# @return [Boolean] true if the class name is safe, false otherwise
111-
def valid_auth_plugin_class_name?(class_name)
112-
# Must be a string
113-
return false unless class_name.is_a?(String)
114-
115-
# Must not be empty or only whitespace
116-
return false if class_name.strip.empty?
117-
118-
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase
119-
# Examples: MyAuthPlugin, SomeCoolAuthPlugin, CustomAuth
120-
return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
121-
122-
# Must not be a system/built-in class name
123-
return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
124-
125-
true
126-
end
12746
end
12847
end
12948
end

lib/hooks/app/helpers.rb

Lines changed: 3 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -65,69 +65,19 @@ def parse_payload(raw_body, headers, symbolize: true)
6565
# Load handler class
6666
#
6767
# @param handler_class_name [String] The name of the handler class to load
68-
# @param handler_dir [String] The directory containing handler files (fallback for dynamic loading)
68+
# @param handler_dir [String] The directory containing handler files (unused - kept for compatibility)
6969
# @return [Object] An instance of the loaded handler class
7070
# @raise [StandardError] If handler cannot be found
7171
def load_handler(handler_class_name, handler_dir = nil)
72-
# Try to get handler class from loaded plugins registry first
72+
# Get handler class from loaded plugins registry (boot-time loaded only)
7373
begin
7474
handler_class = Core::PluginLoader.get_handler_plugin(handler_class_name)
7575
return handler_class.new
7676
rescue => e
77-
# If not found in registry and handler_dir is provided, fall back to dynamic loading
78-
if handler_dir
79-
return load_handler_dynamically(handler_class_name, handler_dir)
80-
else
81-
error!("failed to get handler '#{handler_class_name}': #{e.message}", 500)
82-
end
77+
error!("failed to get handler '#{handler_class_name}': #{e.message}", 500)
8378
end
8479
end
8580

86-
private
87-
88-
# Load handler class dynamically (fallback for backward compatibility)
89-
#
90-
# @param handler_class_name [String] The name of the handler class to load
91-
# @param handler_dir [String] The directory containing handler files
92-
# @return [Object] An instance of the loaded handler class
93-
# @raise [LoadError] If the handler file or class cannot be found
94-
# @raise [StandardError] Halts with error if handler cannot be loaded
95-
def load_handler_dynamically(handler_class_name, handler_dir)
96-
# Security: Validate handler class name to prevent arbitrary class loading
97-
unless valid_handler_class_name?(handler_class_name)
98-
error!("invalid handler class name: #{handler_class_name}", 400)
99-
end
100-
101-
# Convert class name to file name (e.g., Team1Handler -> team1_handler.rb)
102-
# E.g.2: GithubHandler -> github_handler.rb
103-
# E.g.3: GitHubHandler -> git_hub_handler.rb
104-
file_name = handler_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + ".rb"
105-
file_path = File.join(handler_dir, file_name)
106-
107-
# Security: Ensure the file path doesn't escape the handler directory
108-
normalized_handler_dir = Pathname.new(File.expand_path(handler_dir))
109-
normalized_file_path = Pathname.new(File.expand_path(file_path))
110-
unless normalized_file_path.descend.any? { |path| path == normalized_handler_dir }
111-
error!("handler path outside of handler directory", 400)
112-
end
113-
114-
if File.exist?(file_path)
115-
require file_path
116-
handler_class = Object.const_get(handler_class_name)
117-
118-
# Security: Ensure the loaded class inherits from the expected base class
119-
unless handler_class < Hooks::Plugins::Handlers::Base
120-
error!("handler class must inherit from Hooks::Plugins::Handlers::Base", 400)
121-
end
122-
123-
handler_class.new
124-
else
125-
raise LoadError, "Handler #{handler_class_name} not found at #{file_path}"
126-
end
127-
rescue => e
128-
error!("failed to load handler: #{e.message}", 500)
129-
end
130-
13181
public
13282

13383
# Load auth plugin class (DEPRECATED - plugins are now loaded at boot time)
@@ -143,48 +93,6 @@ def load_auth_plugin(auth_plugin_class_name, auth_plugin_dir)
14393

14494
private
14595

146-
# Validate that a handler class name is safe to load
147-
#
148-
# @param class_name [String] The class name to validate
149-
# @return [Boolean] true if the class name is safe, false otherwise
150-
def valid_handler_class_name?(class_name)
151-
# Must be a string
152-
return false unless class_name.is_a?(String)
153-
154-
# Must not be empty or only whitespace
155-
return false if class_name.strip.empty?
156-
157-
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase
158-
# Examples: MyHandler, GitHubHandler, Team1Handler
159-
return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
160-
161-
# Must not be a system/built-in class name
162-
return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
163-
164-
true
165-
end
166-
167-
# Validate that an auth plugin class name is safe to load
168-
#
169-
# @param class_name [String] The class name to validate
170-
# @return [Boolean] true if the class name is safe, false otherwise
171-
def valid_auth_plugin_class_name?(class_name)
172-
# Must be a string
173-
return false unless class_name.is_a?(String)
174-
175-
# Must not be empty or only whitespace
176-
return false if class_name.strip.empty?
177-
178-
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase
179-
# Examples: MyAuthPlugin, SomeCoolAuthPlugin, CustomAuth
180-
return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
181-
182-
# Must not be a system/built-in class name
183-
return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
184-
185-
true
186-
end
187-
18896
# Determine HTTP error code from exception
18997
#
19098
# @param exception [Exception] The exception to map to an HTTP status code

spec/unit/lib/hooks/app/auth/custom_auth_plugin_spec.rb

Lines changed: 19 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,28 @@ def error!(message, code)
3030
before do
3131
# Create temporary directory for custom auth plugins
3232
FileUtils.mkdir_p(custom_auth_plugin_dir)
33+
# Clear plugin registries before each test
34+
Hooks::Core::PluginLoader.clear_plugins
3335
end
3436

3537
after do
3638
# Clean up
3739
FileUtils.rm_rf(custom_auth_plugin_dir) if Dir.exist?(custom_auth_plugin_dir)
40+
# Clear plugin registries after each test
41+
Hooks::Core::PluginLoader.clear_plugins
3842
end
3943

40-
context "when custom auth plugin is configured but directory not set" do
41-
it "falls back to POC error message" do
44+
context "when custom auth plugin is configured but not loaded at boot time" do
45+
it "returns unsupported auth type error" do
4246
endpoint_config = { auth: { type: "some_cool_auth_plugin" } }
43-
empty_global_config = {}
4447

4548
expect do
46-
instance.validate_auth!(payload, headers, endpoint_config, empty_global_config)
49+
instance.validate_auth!(payload, headers, endpoint_config, global_config)
4750
end.to raise_error(StandardError, /unsupported auth type/)
4851
end
4952
end
5053

51-
context "when custom auth plugin exists and is valid" do
54+
context "when custom auth plugin exists and is loaded at boot time" do
5255
let(:plugin_file_content) do
5356
<<~RUBY
5457
module Hooks
@@ -68,9 +71,11 @@ def self.valid?(payload:, headers:, config:)
6871

6972
before do
7073
File.write(File.join(custom_auth_plugin_dir, "some_cool_auth_plugin.rb"), plugin_file_content)
74+
# Load plugins at boot time as would happen in real application
75+
Hooks::Core::PluginLoader.load_all_plugins(global_config)
7176
end
7277

73-
it "loads and uses the custom auth plugin successfully" do
78+
it "uses the custom auth plugin successfully" do
7479
endpoint_config = { auth: { type: "some_cool_auth_plugin" } }
7580

7681
expect do
@@ -99,6 +104,8 @@ def self.valid?(payload:, headers:, config:)
99104

100105
before do
101106
File.write(File.join(custom_auth_plugin_dir, "failing_auth_plugin.rb"), plugin_file_content)
107+
# Load plugins at boot time as would happen in real application
108+
Hooks::Core::PluginLoader.load_all_plugins(global_config)
102109
end
103110

104111
it "returns authentication failed error" do
@@ -110,67 +117,17 @@ def self.valid?(payload:, headers:, config:)
110117
end
111118
end
112119

113-
context "when custom auth plugin file does not exist" do
114-
it "returns custom plugin loading error" do
120+
context "when custom auth plugin file does not exist at boot time" do
121+
it "returns unsupported auth type error" do
115122
endpoint_config = { auth: { type: "nonexistent_plugin" } }
116123

117124
expect do
118125
instance.validate_auth!(payload, headers, endpoint_config, global_config)
119-
end.to raise_error(StandardError, /Auth plugin NonexistentPlugin not found/)
120-
end
121-
end
122-
123-
context "when custom auth plugin has security issues" do
124-
context "with invalid class name" do
125-
it "converts lowercase plugin name and fails to find file" do
126-
endpoint_config = { auth: { type: "lowercase_plugin" } }
127-
128-
expect do
129-
instance.validate_auth!(payload, headers, endpoint_config, global_config)
130-
end.to raise_error(StandardError, /Auth plugin LowercasePlugin not found/)
131-
end
132-
133-
it "rejects plugin with special characters" do
134-
endpoint_config = { auth: { type: "plugin$bad" } }
135-
136-
expect do
137-
instance.validate_auth!(payload, headers, endpoint_config, global_config)
138-
end.to raise_error(StandardError, /invalid auth plugin type/)
139-
end
140-
end
141-
142-
context "with plugin that doesn't inherit from Base" do
143-
let(:bad_plugin_file_content) do
144-
<<~RUBY
145-
module Hooks
146-
module Plugins
147-
module Auth
148-
class BadPlugin
149-
def self.valid?(payload:, headers:, config:)
150-
true
151-
end
152-
end
153-
end
154-
end
155-
end
156-
RUBY
157-
end
158-
159-
before do
160-
File.write(File.join(custom_auth_plugin_dir, "bad_plugin.rb"), bad_plugin_file_content)
161-
end
162-
163-
it "rejects plugin that doesn't inherit from Base" do
164-
endpoint_config = { auth: { type: "bad_plugin" } }
165-
166-
expect do
167-
instance.validate_auth!(payload, headers, endpoint_config, global_config)
168-
end.to raise_error(StandardError, /auth plugin class must inherit from/)
169-
end
126+
end.to raise_error(StandardError, /unsupported auth type/)
170127
end
171128
end
172129

173-
context "with complex plugin names" do
130+
context "with complex plugin names loaded at boot time" do
174131
let(:plugin_file_content) do
175132
<<~RUBY
176133
module Hooks
@@ -189,6 +146,8 @@ def self.valid?(payload:, headers:, config:)
189146

190147
before do
191148
File.write(File.join(custom_auth_plugin_dir, "git_hub_o_auth2_plugin.rb"), plugin_file_content)
149+
# Load plugins at boot time as would happen in real application
150+
Hooks::Core::PluginLoader.load_all_plugins(global_config)
192151
end
193152

194153
it "handles complex CamelCase names correctly" do

0 commit comments

Comments
 (0)