vector_mcp 0.3.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +281 -0
- data/README.md +292 -501
- data/lib/vector_mcp/errors.rb +24 -0
- data/lib/vector_mcp/handlers/core.rb +89 -6
- data/lib/vector_mcp/logging/component.rb +131 -0
- data/lib/vector_mcp/logging/configuration.rb +156 -0
- data/lib/vector_mcp/logging/constants.rb +21 -0
- data/lib/vector_mcp/logging/core.rb +175 -0
- data/lib/vector_mcp/logging/filters/component.rb +69 -0
- data/lib/vector_mcp/logging/filters/level.rb +23 -0
- data/lib/vector_mcp/logging/formatters/base.rb +52 -0
- data/lib/vector_mcp/logging/formatters/json.rb +83 -0
- data/lib/vector_mcp/logging/formatters/text.rb +72 -0
- data/lib/vector_mcp/logging/outputs/base.rb +64 -0
- data/lib/vector_mcp/logging/outputs/console.rb +35 -0
- data/lib/vector_mcp/logging/outputs/file.rb +157 -0
- data/lib/vector_mcp/logging.rb +71 -0
- data/lib/vector_mcp/security/auth_manager.rb +79 -0
- data/lib/vector_mcp/security/authorization.rb +96 -0
- data/lib/vector_mcp/security/middleware.rb +172 -0
- data/lib/vector_mcp/security/session_context.rb +147 -0
- data/lib/vector_mcp/security/strategies/api_key.rb +167 -0
- data/lib/vector_mcp/security/strategies/custom.rb +71 -0
- data/lib/vector_mcp/security/strategies/jwt_token.rb +118 -0
- data/lib/vector_mcp/security.rb +46 -0
- data/lib/vector_mcp/server.rb +141 -1
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +35 -2
- metadata +55 -3
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
# Manages authorization policies for VectorMCP servers
|
6
|
+
# Provides fine-grained access control for tools and resources
|
7
|
+
class Authorization
|
8
|
+
attr_reader :policies, :enabled
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@policies = {}
|
12
|
+
@enabled = false
|
13
|
+
end
|
14
|
+
|
15
|
+
# Enable authorization system
|
16
|
+
def enable!
|
17
|
+
@enabled = true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Disable authorization (return to pass-through mode)
|
21
|
+
def disable!
|
22
|
+
@enabled = false
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add an authorization policy for a resource type
|
26
|
+
# @param resource_type [Symbol] the type of resource (e.g., :tool, :resource, :prompt)
|
27
|
+
# @param block [Proc] the policy block that receives (user, action, resource)
|
28
|
+
def add_policy(resource_type, &block)
|
29
|
+
@policies[resource_type] = block
|
30
|
+
end
|
31
|
+
|
32
|
+
# Remove an authorization policy
|
33
|
+
# @param resource_type [Symbol] the resource type to remove policy for
|
34
|
+
def remove_policy(resource_type)
|
35
|
+
@policies.delete(resource_type)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check if a user is authorized to perform an action on a resource
|
39
|
+
# @param user [Object] the authenticated user object
|
40
|
+
# @param action [Symbol] the action being attempted (e.g., :call, :read, :list)
|
41
|
+
# @param resource [Object] the resource being accessed
|
42
|
+
# @return [Boolean] true if authorized, false otherwise
|
43
|
+
def authorize(user, action, resource)
|
44
|
+
return true unless @enabled
|
45
|
+
|
46
|
+
resource_type = determine_resource_type(resource)
|
47
|
+
policy = @policies[resource_type]
|
48
|
+
|
49
|
+
# If no policy is defined, allow access (opt-in authorization)
|
50
|
+
return true unless policy
|
51
|
+
|
52
|
+
begin
|
53
|
+
policy_result = policy.call(user, action, resource)
|
54
|
+
policy_result ? true : false
|
55
|
+
rescue StandardError
|
56
|
+
# Log error but deny access for safety
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if authorization is required
|
62
|
+
# @return [Boolean] true if authorization is enabled
|
63
|
+
def required?
|
64
|
+
@enabled
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get list of resource types with policies
|
68
|
+
# @return [Array<Symbol>] array of resource types
|
69
|
+
def policy_types
|
70
|
+
@policies.keys
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Determine the resource type from the resource object
|
76
|
+
# @param resource [Object] the resource object
|
77
|
+
# @return [Symbol] the resource type
|
78
|
+
def determine_resource_type(resource)
|
79
|
+
case resource
|
80
|
+
when VectorMCP::Definitions::Tool
|
81
|
+
:tool
|
82
|
+
when VectorMCP::Definitions::Resource
|
83
|
+
:resource
|
84
|
+
when VectorMCP::Definitions::Prompt
|
85
|
+
:prompt
|
86
|
+
when VectorMCP::Definitions::Root
|
87
|
+
:root
|
88
|
+
else
|
89
|
+
# Try to infer from class name
|
90
|
+
class_name = resource.class.name.split("::").last&.downcase
|
91
|
+
class_name&.to_sym || :unknown
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
# Security middleware for request authentication and authorization
|
6
|
+
# Integrates with transport layers to provide security controls
|
7
|
+
class Middleware
|
8
|
+
attr_reader :auth_manager, :authorization
|
9
|
+
|
10
|
+
# Initialize middleware with auth components
|
11
|
+
# @param auth_manager [AuthManager] the authentication manager
|
12
|
+
# @param authorization [Authorization] the authorization manager
|
13
|
+
def initialize(auth_manager, authorization)
|
14
|
+
@auth_manager = auth_manager
|
15
|
+
@authorization = authorization
|
16
|
+
end
|
17
|
+
|
18
|
+
# Authenticate a request and return session context
|
19
|
+
# @param request [Hash] the request object
|
20
|
+
# @param strategy [Symbol] optional authentication strategy override
|
21
|
+
# @return [SessionContext] the session context for the request
|
22
|
+
def authenticate_request(request, strategy: nil)
|
23
|
+
auth_result = @auth_manager.authenticate(request, strategy: strategy)
|
24
|
+
SessionContext.from_auth_result(auth_result)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if a session is authorized for an action on a resource
|
28
|
+
# @param session_context [SessionContext] the session context
|
29
|
+
# @param action [Symbol] the action being attempted
|
30
|
+
# @param resource [Object] the resource being accessed
|
31
|
+
# @return [Boolean] true if authorized
|
32
|
+
def authorize_action(session_context, action, resource)
|
33
|
+
# Always allow if authorization is disabled
|
34
|
+
return true unless @authorization.required?
|
35
|
+
|
36
|
+
# Check authorization policy
|
37
|
+
@authorization.authorize(session_context.user, action, resource)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Process a request through the complete security pipeline
|
41
|
+
# @param request [Hash] the request object
|
42
|
+
# @param action [Symbol] the action being attempted
|
43
|
+
# @param resource [Object] the resource being accessed
|
44
|
+
# @return [Hash] result with session_context and authorization status
|
45
|
+
def process_request(request, action: :access, resource: nil)
|
46
|
+
# Step 1: Authenticate the request
|
47
|
+
session_context = authenticate_request(request)
|
48
|
+
|
49
|
+
# Step 2: Check if authentication is required but failed
|
50
|
+
if @auth_manager.required? && !session_context.authenticated?
|
51
|
+
return {
|
52
|
+
success: false,
|
53
|
+
error: "Authentication required",
|
54
|
+
error_code: "AUTHENTICATION_REQUIRED",
|
55
|
+
session_context: session_context
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Step 3: Check authorization if resource is provided
|
60
|
+
if resource && !authorize_action(session_context, action, resource)
|
61
|
+
return {
|
62
|
+
success: false,
|
63
|
+
error: "Access denied",
|
64
|
+
error_code: "AUTHORIZATION_FAILED",
|
65
|
+
session_context: session_context
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Step 4: Success
|
70
|
+
{
|
71
|
+
success: true,
|
72
|
+
session_context: session_context
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Create a request object from different transport formats
|
77
|
+
# @param transport_request [Object] the transport-specific request
|
78
|
+
# @return [Hash] normalized request object
|
79
|
+
def normalize_request(transport_request)
|
80
|
+
case transport_request
|
81
|
+
when Hash
|
82
|
+
# Check if it's a Rack environment (has REQUEST_METHOD key)
|
83
|
+
if transport_request.key?("REQUEST_METHOD")
|
84
|
+
extract_from_rack_env(transport_request)
|
85
|
+
else
|
86
|
+
# Already normalized
|
87
|
+
transport_request
|
88
|
+
end
|
89
|
+
else
|
90
|
+
# Extract from transport-specific request (e.g., custom objects)
|
91
|
+
extract_request_data(transport_request)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Check if security is enabled
|
96
|
+
# @return [Boolean] true if any security features are enabled
|
97
|
+
def security_enabled?
|
98
|
+
@auth_manager.required? || @authorization.required?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get security status for debugging/monitoring
|
102
|
+
# @return [Hash] current security configuration status
|
103
|
+
def security_status
|
104
|
+
{
|
105
|
+
authentication: {
|
106
|
+
enabled: @auth_manager.required?,
|
107
|
+
strategies: @auth_manager.available_strategies,
|
108
|
+
default_strategy: @auth_manager.default_strategy
|
109
|
+
},
|
110
|
+
authorization: {
|
111
|
+
enabled: @authorization.required?,
|
112
|
+
policy_types: @authorization.policy_types
|
113
|
+
}
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Extract request data from transport-specific formats
|
120
|
+
# @param transport_request [Object] the transport request
|
121
|
+
# @return [Hash] extracted request data
|
122
|
+
def extract_request_data(transport_request)
|
123
|
+
# Handle Rack environment (for SSE transport)
|
124
|
+
if transport_request.respond_to?(:[]) && transport_request["REQUEST_METHOD"]
|
125
|
+
extract_from_rack_env(transport_request)
|
126
|
+
else
|
127
|
+
# Default fallback
|
128
|
+
{ headers: {}, params: {} }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Extract data from Rack environment
|
133
|
+
# @param env [Hash] the Rack environment
|
134
|
+
# @return [Hash] extracted request data
|
135
|
+
def extract_from_rack_env(env)
|
136
|
+
# Extract headers (HTTP_ prefixed in Rack env)
|
137
|
+
headers = {}
|
138
|
+
env.each do |key, value|
|
139
|
+
next unless key.start_with?("HTTP_")
|
140
|
+
|
141
|
+
# Convert HTTP_X_API_KEY to X-API-Key format
|
142
|
+
header_name = key[5..].split("_").map do |part|
|
143
|
+
case part.upcase
|
144
|
+
when "API" then "API" # Keep API in all caps
|
145
|
+
else part.capitalize
|
146
|
+
end
|
147
|
+
end.join("-")
|
148
|
+
headers[header_name] = value
|
149
|
+
end
|
150
|
+
|
151
|
+
# Add special headers
|
152
|
+
headers["Authorization"] = env["HTTP_AUTHORIZATION"] if env["HTTP_AUTHORIZATION"]
|
153
|
+
headers["Content-Type"] = env["CONTENT_TYPE"] if env["CONTENT_TYPE"]
|
154
|
+
|
155
|
+
# Extract query parameters
|
156
|
+
params = {}
|
157
|
+
if env["QUERY_STRING"]
|
158
|
+
require "uri"
|
159
|
+
params = URI.decode_www_form(env["QUERY_STRING"]).to_h
|
160
|
+
end
|
161
|
+
|
162
|
+
{
|
163
|
+
headers: headers,
|
164
|
+
params: params,
|
165
|
+
method: env["REQUEST_METHOD"],
|
166
|
+
path: env["PATH_INFO"],
|
167
|
+
rack_env: env
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
# Represents the security context for a user session
|
6
|
+
# Contains authentication and authorization information
|
7
|
+
class SessionContext
|
8
|
+
attr_reader :user, :authenticated, :permissions, :auth_strategy, :authenticated_at
|
9
|
+
|
10
|
+
# Initialize session context
|
11
|
+
# @param user [Object] the authenticated user object
|
12
|
+
# @param authenticated [Boolean] whether the user is authenticated
|
13
|
+
# @param auth_strategy [String] the authentication strategy used
|
14
|
+
# @param authenticated_at [Time] when authentication occurred
|
15
|
+
def initialize(user: nil, authenticated: false, auth_strategy: nil, authenticated_at: nil)
|
16
|
+
@user = user
|
17
|
+
@authenticated = authenticated
|
18
|
+
@auth_strategy = auth_strategy
|
19
|
+
@authenticated_at = authenticated_at || Time.now
|
20
|
+
@permissions = Set.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if the session is authenticated
|
24
|
+
# @return [Boolean] true if authenticated
|
25
|
+
def authenticated?
|
26
|
+
@authenticated
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if the user has a specific permission
|
30
|
+
# @param permission [String, Symbol] the permission to check
|
31
|
+
# @return [Boolean] true if user has the permission
|
32
|
+
def can?(permission)
|
33
|
+
@permissions.include?(permission.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check if the user can perform an action on a resource
|
37
|
+
# @param action [String, Symbol] the action (e.g., 'read', 'write', 'execute')
|
38
|
+
# @param resource [String, Symbol] the resource (e.g., 'tools', 'resources')
|
39
|
+
# @return [Boolean] true if user can perform the action
|
40
|
+
def can_access?(action, resource)
|
41
|
+
can?("#{action}:#{resource}") || can?("#{action}:*") || can?("*:#{resource}") || can?("*:*")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add a permission to the session
|
45
|
+
# @param permission [String, Symbol] the permission to add
|
46
|
+
def add_permission(permission)
|
47
|
+
@permissions << permission.to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add multiple permissions to the session
|
51
|
+
# @param permissions [Array<String, Symbol>] the permissions to add
|
52
|
+
def add_permissions(permissions)
|
53
|
+
permissions.each { |perm| add_permission(perm) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Remove a permission from the session
|
57
|
+
# @param permission [String, Symbol] the permission to remove
|
58
|
+
def remove_permission(permission)
|
59
|
+
@permissions.delete(permission.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Clear all permissions
|
63
|
+
def clear_permissions
|
64
|
+
@permissions.clear
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get user identifier for logging/auditing
|
68
|
+
# @return [String] a string identifying the user
|
69
|
+
def user_identifier
|
70
|
+
return "anonymous" unless authenticated?
|
71
|
+
return "anonymous" if @user.nil?
|
72
|
+
|
73
|
+
case @user
|
74
|
+
when Hash
|
75
|
+
@user[:user_id] || @user[:sub] || @user[:email] || @user[:api_key] || "authenticated_user"
|
76
|
+
when String
|
77
|
+
@user
|
78
|
+
else
|
79
|
+
@user.respond_to?(:id) ? @user.id.to_s : "authenticated_user"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get authentication method used
|
84
|
+
# @return [String] the authentication strategy
|
85
|
+
def auth_method
|
86
|
+
@auth_strategy || "none"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Check if authentication is recent (within specified seconds)
|
90
|
+
# @param max_age [Integer] maximum age in seconds (default: 3600 = 1 hour)
|
91
|
+
# @return [Boolean] true if authentication is recent
|
92
|
+
def auth_recent?(max_age: 3600)
|
93
|
+
return false unless authenticated?
|
94
|
+
|
95
|
+
(Time.now - @authenticated_at) <= max_age
|
96
|
+
end
|
97
|
+
|
98
|
+
# Convert to hash for serialization
|
99
|
+
# @return [Hash] session context as hash
|
100
|
+
def to_h
|
101
|
+
{
|
102
|
+
authenticated: @authenticated,
|
103
|
+
user_identifier: user_identifier,
|
104
|
+
auth_strategy: @auth_strategy,
|
105
|
+
authenticated_at: @authenticated_at&.iso8601,
|
106
|
+
permissions: @permissions.to_a
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Create an anonymous (unauthenticated) session context
|
111
|
+
# @return [SessionContext] an unauthenticated session
|
112
|
+
def self.anonymous
|
113
|
+
new(authenticated: false)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Create an authenticated session context from auth result
|
117
|
+
# @param auth_result [Hash] the authentication result
|
118
|
+
# @return [SessionContext] an authenticated session
|
119
|
+
def self.from_auth_result(auth_result)
|
120
|
+
return anonymous unless auth_result&.dig(:authenticated)
|
121
|
+
|
122
|
+
user_data = auth_result[:user]
|
123
|
+
|
124
|
+
# Handle special marker for authenticated nil user
|
125
|
+
if user_data == :authenticated_nil_user
|
126
|
+
new(
|
127
|
+
user: nil,
|
128
|
+
authenticated: true,
|
129
|
+
auth_strategy: "custom",
|
130
|
+
authenticated_at: Time.now
|
131
|
+
)
|
132
|
+
else
|
133
|
+
# Extract strategy and authenticated_at only if user_data is a Hash
|
134
|
+
strategy = user_data.is_a?(Hash) ? user_data[:strategy] : nil
|
135
|
+
auth_time = user_data.is_a?(Hash) ? user_data[:authenticated_at] : nil
|
136
|
+
|
137
|
+
new(
|
138
|
+
user: user_data,
|
139
|
+
authenticated: true,
|
140
|
+
auth_strategy: strategy,
|
141
|
+
authenticated_at: auth_time
|
142
|
+
)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
module Strategies
|
6
|
+
# API Key authentication strategy
|
7
|
+
# Supports multiple key formats and storage methods
|
8
|
+
class ApiKey
|
9
|
+
attr_reader :valid_keys
|
10
|
+
|
11
|
+
# Initialize with a list of valid API keys
|
12
|
+
# @param keys [Array<String>] array of valid API keys
|
13
|
+
def initialize(keys: [])
|
14
|
+
@valid_keys = Set.new(keys.map(&:to_s))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add a valid API key
|
18
|
+
# @param key [String] the API key to add
|
19
|
+
def add_key(key)
|
20
|
+
@valid_keys << key.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# Remove an API key
|
24
|
+
# @param key [String] the API key to remove
|
25
|
+
def remove_key(key)
|
26
|
+
@valid_keys.delete(key.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Authenticate a request using API key
|
30
|
+
# @param request [Hash] the request object
|
31
|
+
# @return [Hash, false] user info hash or false if authentication failed
|
32
|
+
def authenticate(request)
|
33
|
+
api_key = extract_api_key(request)
|
34
|
+
return false unless api_key&.length&.positive?
|
35
|
+
|
36
|
+
if @valid_keys.include?(api_key)
|
37
|
+
{
|
38
|
+
api_key: api_key,
|
39
|
+
strategy: "api_key",
|
40
|
+
authenticated_at: Time.now
|
41
|
+
}
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if any keys are configured
|
48
|
+
# @return [Boolean] true if keys are available
|
49
|
+
def configured?
|
50
|
+
!@valid_keys.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get count of configured keys (for debugging)
|
54
|
+
# @return [Integer] number of configured keys
|
55
|
+
def key_count
|
56
|
+
@valid_keys.size
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Extract API key from various request formats
|
62
|
+
# @param request [Hash] the request object
|
63
|
+
# @return [String, nil] the extracted API key
|
64
|
+
def extract_api_key(request)
|
65
|
+
headers = normalize_headers(request)
|
66
|
+
params = normalize_params(request)
|
67
|
+
|
68
|
+
extract_from_headers(headers) || extract_from_params(params)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Normalize headers to handle different formats
|
72
|
+
# @param request [Hash] the request object
|
73
|
+
# @return [Hash] normalized headers
|
74
|
+
def normalize_headers(request)
|
75
|
+
# Check if it's a Rack environment (has REQUEST_METHOD)
|
76
|
+
if request["REQUEST_METHOD"]
|
77
|
+
extract_headers_from_rack_env(request)
|
78
|
+
else
|
79
|
+
request[:headers] || request["headers"] || {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Normalize params to handle different formats
|
84
|
+
# @param request [Hash] the request object
|
85
|
+
# @return [Hash] normalized params
|
86
|
+
def normalize_params(request)
|
87
|
+
# Check if it's a Rack environment (has REQUEST_METHOD)
|
88
|
+
if request["REQUEST_METHOD"]
|
89
|
+
extract_params_from_rack_env(request)
|
90
|
+
else
|
91
|
+
request[:params] || request["params"] || {}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Extract headers from Rack environment
|
96
|
+
# @param env [Hash] the Rack environment
|
97
|
+
# @return [Hash] normalized headers
|
98
|
+
def extract_headers_from_rack_env(env)
|
99
|
+
headers = {}
|
100
|
+
env.each do |key, value|
|
101
|
+
next unless key.start_with?("HTTP_")
|
102
|
+
|
103
|
+
# Convert HTTP_X_API_KEY to X-API-Key format
|
104
|
+
header_name = key[5..].split("_").map do |part|
|
105
|
+
case part.upcase
|
106
|
+
when "API" then "API" # Keep API in all caps
|
107
|
+
else part.capitalize
|
108
|
+
end
|
109
|
+
end.join("-")
|
110
|
+
headers[header_name] = value
|
111
|
+
end
|
112
|
+
|
113
|
+
# Add special headers
|
114
|
+
headers["Authorization"] = env["HTTP_AUTHORIZATION"] if env["HTTP_AUTHORIZATION"]
|
115
|
+
headers["Content-Type"] = env["CONTENT_TYPE"] if env["CONTENT_TYPE"]
|
116
|
+
headers
|
117
|
+
end
|
118
|
+
|
119
|
+
# Extract params from Rack environment
|
120
|
+
# @param env [Hash] the Rack environment
|
121
|
+
# @return [Hash] normalized params
|
122
|
+
def extract_params_from_rack_env(env)
|
123
|
+
params = {}
|
124
|
+
if env["QUERY_STRING"]
|
125
|
+
require "uri"
|
126
|
+
params = URI.decode_www_form(env["QUERY_STRING"]).to_h
|
127
|
+
end
|
128
|
+
params
|
129
|
+
end
|
130
|
+
|
131
|
+
# Extract API key from headers
|
132
|
+
# @param headers [Hash] request headers
|
133
|
+
# @return [String, nil] the API key if found
|
134
|
+
def extract_from_headers(headers)
|
135
|
+
# 1. X-API-Key header (most common)
|
136
|
+
api_key = headers["X-API-Key"] || headers["x-api-key"]
|
137
|
+
return api_key if api_key
|
138
|
+
|
139
|
+
# 2. Authorization header
|
140
|
+
extract_from_auth_header(headers["Authorization"] || headers["authorization"])
|
141
|
+
end
|
142
|
+
|
143
|
+
# Extract API key from Authorization header
|
144
|
+
# @param auth_header [String, nil] the authorization header value
|
145
|
+
# @return [String, nil] the API key if found
|
146
|
+
def extract_from_auth_header(auth_header)
|
147
|
+
return nil unless auth_header
|
148
|
+
|
149
|
+
# Bearer token format
|
150
|
+
return auth_header[7..] if auth_header.start_with?("Bearer ")
|
151
|
+
|
152
|
+
# API-Key scheme format
|
153
|
+
return auth_header[8..] if auth_header.start_with?("API-Key ")
|
154
|
+
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
|
158
|
+
# Extract API key from query parameters
|
159
|
+
# @param params [Hash] request parameters
|
160
|
+
# @return [String, nil] the API key if found
|
161
|
+
def extract_from_params(params)
|
162
|
+
params["api_key"] || params["apikey"]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
module Strategies
|
6
|
+
# Custom authentication strategy
|
7
|
+
# Allows developers to implement their own authentication logic
|
8
|
+
class Custom
|
9
|
+
attr_reader :handler
|
10
|
+
|
11
|
+
# Initialize with a custom authentication handler
|
12
|
+
# @param handler [Proc] a block that takes a request and returns user info or false
|
13
|
+
def initialize(&handler)
|
14
|
+
raise ArgumentError, "Custom authentication strategy requires a block" unless handler
|
15
|
+
|
16
|
+
@handler = handler
|
17
|
+
end
|
18
|
+
|
19
|
+
# Authenticate a request using the custom handler
|
20
|
+
# @param request [Hash] the request object
|
21
|
+
# @return [Object, false] result from custom handler or false if authentication failed
|
22
|
+
def authenticate(request)
|
23
|
+
result = @handler.call(request)
|
24
|
+
|
25
|
+
# Ensure result includes strategy info if it's successful
|
26
|
+
if result && result != false
|
27
|
+
format_successful_result(result)
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
rescue NoMemoryError, StandardError
|
32
|
+
# Log error but return false for security
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check if handler is configured
|
37
|
+
# @return [Boolean] true if handler is present
|
38
|
+
def configured?
|
39
|
+
!@handler.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Format successful authentication result with strategy metadata
|
45
|
+
# @param result [Object] the result from the custom handler
|
46
|
+
# @return [Object] formatted result with strategy metadata
|
47
|
+
def format_successful_result(result)
|
48
|
+
case result
|
49
|
+
when Hash
|
50
|
+
# If result has a :user key, extract it and use as main user data
|
51
|
+
if result.key?(:user)
|
52
|
+
user_data = result[:user]
|
53
|
+
# For nil user, return a marker that will become nil in session context
|
54
|
+
return :authenticated_nil_user if user_data.nil?
|
55
|
+
|
56
|
+
user_data
|
57
|
+
else
|
58
|
+
result.merge(strategy: "custom", authenticated_at: Time.now)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
{
|
62
|
+
user: result,
|
63
|
+
strategy: "custom",
|
64
|
+
authenticated_at: Time.now
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|