First commit!
This commit is contained in:
81
app/services/dsn_authentication_service.rb
Normal file
81
app/services/dsn_authentication_service.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DsnAuthenticationService
|
||||
class AuthenticationError < StandardError; end
|
||||
|
||||
def self.authenticate(request, project_id)
|
||||
# Try multiple authentication methods in order of preference
|
||||
|
||||
# Method 1: Query parameter authentication
|
||||
public_key = extract_key_from_query_params(request)
|
||||
return find_project(public_key, project_id) if public_key
|
||||
|
||||
# Method 2: X-Baffle-Auth header (similar to X-Sentry-Auth)
|
||||
public_key = extract_key_from_baffle_auth_header(request)
|
||||
return find_project(public_key, project_id) if public_key
|
||||
|
||||
# Method 3: Authorization Bearer token
|
||||
public_key = extract_key_from_authorization_header(request)
|
||||
return find_project(public_key, project_id) if public_key
|
||||
|
||||
# Method 4: Basic auth (username is the public_key)
|
||||
public_key = extract_key_from_basic_auth(request)
|
||||
return find_project(public_key, project_id) if public_key
|
||||
|
||||
raise AuthenticationError, "No valid authentication method found"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.extract_key_from_query_params(request)
|
||||
# Support both baffle_key and sentry_key for compatibility
|
||||
request.GET['baffle_key'] || request.GET['sentry_key'] || request.GET['glitchtip_key']
|
||||
end
|
||||
|
||||
def self.extract_key_from_baffle_auth_header(request)
|
||||
auth_header = request.headers['X-Baffle-Auth'] || request.headers['X-Sentry-Auth']
|
||||
return nil unless auth_header
|
||||
|
||||
# Parse: Baffle baffle_key=public_key, baffle_version=1
|
||||
# Or: Sentry sentry_key=public_key, sentry_version=7
|
||||
match = auth_header.match(/(?:baffle_key|sentry_key)=([^,\s]+)/)
|
||||
match&.[](1)
|
||||
end
|
||||
|
||||
def self.extract_key_from_authorization_header(request)
|
||||
authorization_header = request.headers['Authorization']
|
||||
return nil unless authorization_header
|
||||
|
||||
# Parse: Bearer public_key
|
||||
if authorization_header.start_with?('Bearer ')
|
||||
authorization_header[7..-1].strip
|
||||
end
|
||||
end
|
||||
|
||||
def self.extract_key_from_basic_auth(request)
|
||||
authorization_header = request.headers['Authorization']
|
||||
return nil unless authorization_header&.start_with?('Basic ')
|
||||
|
||||
# Decode basic auth: username:password (password is ignored)
|
||||
credentials = Base64.decode64(authorization_header[6..-1])
|
||||
username = credentials.split(':').first
|
||||
username
|
||||
end
|
||||
|
||||
def self.find_project(public_key, project_id)
|
||||
return nil unless public_key.present? && project_id.present?
|
||||
|
||||
# Find project by public_key first
|
||||
project = Project.find_by(public_key: public_key)
|
||||
raise AuthenticationError, "Invalid public_key" unless project
|
||||
|
||||
# Verify project_id matches (supports both slug and ID)
|
||||
project_matches = Project.find_by_project_id(project_id)
|
||||
raise AuthenticationError, "Invalid project_id" unless project_matches == project
|
||||
|
||||
# Ensure project is enabled
|
||||
raise AuthenticationError, "Project is disabled" unless project.enabled?
|
||||
|
||||
project
|
||||
end
|
||||
end
|
||||
122
app/services/event_normalizer.rb
Normal file
122
app/services/event_normalizer.rb
Normal file
@@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EventNormalizer
|
||||
class NormalizationError < StandardError; end
|
||||
|
||||
# Normalize an event by populating all the normalized fields
|
||||
def self.normalize_event!(event)
|
||||
normalizer = new(event)
|
||||
normalizer.normalize!
|
||||
event
|
||||
end
|
||||
|
||||
def initialize(event)
|
||||
@event = event
|
||||
end
|
||||
|
||||
def normalize!
|
||||
return unless @event.present?
|
||||
|
||||
normalize_host
|
||||
normalize_action
|
||||
normalize_method
|
||||
normalize_protocol
|
||||
normalize_path_segments
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_host
|
||||
hostname = extract_hostname
|
||||
return unless hostname
|
||||
|
||||
host = RequestHost.find_or_create_host(hostname)
|
||||
host.increment_usage! unless host.new_record?
|
||||
@event.request_host = host
|
||||
end
|
||||
|
||||
def normalize_action
|
||||
raw_action = @event.instance_variable_get(:@raw_action)
|
||||
return unless raw_action.present?
|
||||
|
||||
action_enum = case raw_action.to_s.downcase
|
||||
when 'allow', 'pass' then :allow
|
||||
when 'deny', 'block' then :deny
|
||||
when 'challenge' then :challenge
|
||||
when 'redirect' then :redirect
|
||||
else :allow
|
||||
end
|
||||
|
||||
@event.action = action_enum
|
||||
end
|
||||
|
||||
def normalize_method
|
||||
raw_method = @event.instance_variable_get(:@raw_request_method)
|
||||
return unless raw_method.present?
|
||||
|
||||
method_enum = case raw_method.to_s.upcase
|
||||
when 'GET' then :get
|
||||
when 'POST' then :post
|
||||
when 'PUT' then :put
|
||||
when 'PATCH' then :patch
|
||||
when 'DELETE' then :delete
|
||||
when 'HEAD' then :head
|
||||
when 'OPTIONS' then :options
|
||||
else :get
|
||||
end
|
||||
|
||||
@event.request_method = method_enum
|
||||
end
|
||||
|
||||
def normalize_protocol
|
||||
raw_protocol = @event.instance_variable_get(:@raw_request_protocol)
|
||||
return unless raw_protocol.present?
|
||||
|
||||
# Store the protocol directly (HTTP/1.1, HTTP/2, etc.)
|
||||
# Could normalize to enum if needed, but keeping as string for flexibility
|
||||
@event.request_protocol = raw_protocol
|
||||
end
|
||||
|
||||
def normalize_path_segments
|
||||
segments = @event.path_segments_array
|
||||
return if segments.empty?
|
||||
|
||||
segment_ids = segments.map do |segment|
|
||||
path_segment = PathSegment.find_or_create_segment(segment)
|
||||
path_segment.increment_usage! unless path_segment.new_record?
|
||||
path_segment.id
|
||||
end
|
||||
|
||||
@event.request_segment_ids = segment_ids
|
||||
end
|
||||
|
||||
def extract_hostname
|
||||
# Try to extract hostname from various sources
|
||||
return @event.request_hostname if @event.respond_to?(:request_hostname)
|
||||
|
||||
# Extract from request URL if available
|
||||
if @event.request_url.present?
|
||||
begin
|
||||
uri = URI.parse(@event.request_url)
|
||||
return uri.hostname
|
||||
rescue URI::InvalidURIError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Extract from payload as fallback
|
||||
if @event.payload.present?
|
||||
url = @event.payload.dig("request", "url")
|
||||
if url.present?
|
||||
begin
|
||||
uri = URI.parse(url)
|
||||
return uri.hostname
|
||||
rescue URI::InvalidURIError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user