auth-sanitizer 0.1.0

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.
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module Sanitizer
5
+ # Mixin that redacts sensitive instance variables in `#inspect` output.
6
+ #
7
+ # Classes include this module and declare which attribute names should be
8
+ # filtered via {.filtered_attributes}. Matching and replacement behavior is
9
+ # delegated to {ThingFilter}, which is initialized once per object.
10
+ #
11
+ # This means existing objects keep the filter configuration that was present
12
+ # when they were initialized, even if global config or class-level filter
13
+ # declarations change later.
14
+ #
15
+ # The label used for redaction is resolved from {Auth::Sanitizer.filtered_label}
16
+ # at object initialization time. Host gems may customize this by installing a
17
+ # provider via {Auth::Sanitizer.filtered_label_provider=}.
18
+ module FilteredAttributes
19
+ class << self
20
+ # Hook invoked when the module is included. Extends the including class with
21
+ # class-level helpers and prepends the initializer hook.
22
+ #
23
+ # @param [Class] base The including class
24
+ # @return [void]
25
+ def included(base)
26
+ base.extend(ClassMethods)
27
+ base.prepend(InitializerMethods)
28
+ end
29
+ end
30
+
31
+ # Initializer hook that snapshots the thing filter for this object.
32
+ #
33
+ # The snapshot captures both the class-level filtered attribute names and
34
+ # the current {Auth::Sanitizer.filtered_label} value.
35
+ module InitializerMethods
36
+ def initialize(*args, &block)
37
+ super(*args, &block)
38
+ @thing_filter = ThingFilter.new(
39
+ self.class.filtered_attribute_names,
40
+ label: Auth::Sanitizer.filtered_label,
41
+ )
42
+ end
43
+ end
44
+
45
+ # Class-level helpers for configuring filtered attributes.
46
+ module ClassMethods
47
+ class << self
48
+ # Declare attributes that should be redacted in inspect output.
49
+ #
50
+ # @param [Array<Symbol, String>] attributes One or more attribute names
51
+ # @return [void]
52
+ def filtered_attributes(base, *attributes)
53
+ base.instance_variable_set(:@filtered_attribute_names, attributes.map(&:to_sym))
54
+ end
55
+
56
+ # The configured attribute names to filter.
57
+ #
58
+ # @param [Class] base The class to get filtered attributes for
59
+ # @return [Array<Symbol>]
60
+ def filtered_attribute_names(base)
61
+ return [] unless base.instance_variable_defined?(:@filtered_attribute_names)
62
+
63
+ base.instance_variable_get(:@filtered_attribute_names) || []
64
+ end
65
+ end
66
+
67
+ # Declare attributes that should be redacted in inspect output.
68
+ #
69
+ # @param [Array<Symbol, String>] attributes One or more attribute names
70
+ # @return [void]
71
+ def filtered_attributes(*attributes)
72
+ ClassMethods.filtered_attributes(self, *attributes)
73
+ end
74
+
75
+ # The configured attribute names to filter.
76
+ #
77
+ # @return [Array<Symbol>]
78
+ def filtered_attribute_names
79
+ ClassMethods.filtered_attribute_names(self)
80
+ end
81
+ end
82
+
83
+ # The initialized thing filter used by this object.
84
+ #
85
+ # This is a per-instance snapshot created during initialization.
86
+ #
87
+ # @return [ThingFilter]
88
+ def thing_filter
89
+ @thing_filter
90
+ end
91
+
92
+ # Custom inspect that redacts configured attributes.
93
+ #
94
+ # @return [String]
95
+ def inspect
96
+ return super if thing_filter.things.empty?
97
+
98
+ inspected_vars = instance_variables.map do |var|
99
+ if thing_filter.filtered?(var)
100
+ "#{var}=#{thing_filter.label}"
101
+ else
102
+ "#{var}=#{instance_variable_get(var).inspect}"
103
+ end
104
+ end
105
+ "#<#{self.class}:#{object_id} #{inspected_vars.join(", ")}>"
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module Sanitizer
5
+ # Logger wrapper that redacts sensitive values from debug output before
6
+ # delegating to the underlying logger instance.
7
+ #
8
+ # This class is intentionally narrow in scope: it only sanitizes string
9
+ # messages emitted through the logging path and leaves request/response
10
+ # behavior unchanged.
11
+ #
12
+ # The underlying {ThingFilter} is initialized once when the logger wrapper is
13
+ # created, so later config changes do not alter the behavior of existing
14
+ # logger instances.
15
+ class SanitizedLogger
16
+ # Create a new sanitized logger wrapper.
17
+ #
18
+ # @param [#add, #debug, #info, #warn, #error, #fatal, #unknown] logger
19
+ # The underlying logger instance that will receive sanitized messages.
20
+ # @param [Array<String>] filtered_keys
21
+ # Key names whose values should be redacted in debug output.
22
+ # Defaults to {Auth::Sanitizer.default_filtered_keys}.
23
+ # @param [String] label
24
+ # Replacement label for redacted values.
25
+ # Defaults to {Auth::Sanitizer.filtered_label}.
26
+ def initialize(logger, filtered_keys: Auth::Sanitizer.default_filtered_keys, label: Auth::Sanitizer.filtered_label)
27
+ @logger = logger
28
+ @thing_filter = ThingFilter.new(filtered_keys, label: label)
29
+ end
30
+
31
+ # Add a log entry after sanitizing any string payloads.
32
+ #
33
+ # @param [Integer, Symbol, String, nil] severity Logger severity
34
+ # @param [Object, nil] message Optional log message
35
+ # @param [Object, nil] progname Optional program name
36
+ # @yieldreturn [Object] Deferred log message
37
+ # @return [Object] The underlying logger result
38
+ def add(severity, message = nil, progname = nil)
39
+ if block_given?
40
+ @logger.add(severity, sanitize(message), sanitize(progname)) { sanitize(yield) }
41
+ else
42
+ @logger.add(severity, sanitize(message), sanitize(progname))
43
+ end
44
+ end
45
+
46
+ # Append a message to the underlying logger after sanitization.
47
+ #
48
+ # @param [String] message Message to append
49
+ # @return [Object] The underlying logger result
50
+ def <<(message)
51
+ @logger << sanitize(message)
52
+ end
53
+
54
+ # Log a debug message after sanitization.
55
+ #
56
+ # @param [Object, nil] progname Optional program name
57
+ # @yieldreturn [Object] Deferred log message
58
+ # @return [Object] The underlying logger result
59
+ def debug(progname = nil, &block)
60
+ log(:debug, progname, &block)
61
+ end
62
+
63
+ # Log an info message after sanitization.
64
+ #
65
+ # @param [Object, nil] progname Optional program name
66
+ # @yieldreturn [Object] Deferred log message
67
+ # @return [Object] The underlying logger result
68
+ def info(progname = nil, &block)
69
+ log(:info, progname, &block)
70
+ end
71
+
72
+ # Log a warning message after sanitization.
73
+ #
74
+ # @param [Object, nil] progname Optional program name
75
+ # @yieldreturn [Object] Deferred log message
76
+ # @return [Object] The underlying logger result
77
+ def warn(progname = nil, &block)
78
+ log(:warn, progname, &block)
79
+ end
80
+
81
+ # Log an error message after sanitization.
82
+ #
83
+ # @param [Object, nil] progname Optional program name
84
+ # @yieldreturn [Object] Deferred log message
85
+ # @return [Object] The underlying logger result
86
+ def error(progname = nil, &block)
87
+ log(:error, progname, &block)
88
+ end
89
+
90
+ # Log a fatal message after sanitization.
91
+ #
92
+ # @param [Object, nil] progname Optional program name
93
+ # @yieldreturn [Object] Deferred log message
94
+ # @return [Object] The underlying logger result
95
+ def fatal(progname = nil, &block)
96
+ log(:fatal, progname, &block)
97
+ end
98
+
99
+ # Log an unknown-severity message after sanitization.
100
+ #
101
+ # @param [Object, nil] progname Optional program name
102
+ # @yieldreturn [Object] Deferred log message
103
+ # @return [Object] The underlying logger result
104
+ def unknown(progname = nil, &block)
105
+ log(:unknown, progname, &block)
106
+ end
107
+
108
+ # Close the underlying logger if supported.
109
+ #
110
+ # @return [void]
111
+ def close
112
+ @logger.close if @logger.respond_to?(:close)
113
+ end
114
+
115
+ # Access the formatter of the underlying logger if supported.
116
+ #
117
+ # @return [Object, nil]
118
+ def formatter
119
+ @logger.formatter if @logger.respond_to?(:formatter)
120
+ end
121
+
122
+ # Set the formatter of the underlying logger if supported.
123
+ #
124
+ # @param [Object] formatter Formatter object
125
+ # @return [void]
126
+ def formatter=(formatter)
127
+ @logger.formatter = formatter if @logger.respond_to?(:formatter=)
128
+ end
129
+
130
+ # Access the logger level if supported.
131
+ #
132
+ # @return [Object, nil]
133
+ def level
134
+ @logger.level if @logger.respond_to?(:level)
135
+ end
136
+
137
+ # Set the logger level if supported.
138
+ #
139
+ # @param [Object] level Logger level
140
+ # @return [void]
141
+ def level=(level)
142
+ @logger.level = level if @logger.respond_to?(:level=)
143
+ end
144
+
145
+ # Access the logger progname if supported.
146
+ #
147
+ # @return [Object, nil]
148
+ def progname
149
+ @logger.progname if @logger.respond_to?(:progname)
150
+ end
151
+
152
+ # Set the logger progname if supported.
153
+ #
154
+ # @param [Object] progname Logger progname
155
+ # @return [void]
156
+ def progname=(progname)
157
+ @logger.progname = progname if @logger.respond_to?(:progname=)
158
+ end
159
+
160
+ # Report support for methods provided by the wrapped logger.
161
+ #
162
+ # @param [Symbol] method_name Method name to check
163
+ # @param [Boolean] include_private Whether private methods are considered
164
+ # @return [Boolean]
165
+ def respond_to_missing?(method_name, include_private = false)
166
+ @logger.respond_to?(method_name, include_private) || super
167
+ end
168
+
169
+ # Delegate unsupported methods to the wrapped logger.
170
+ #
171
+ # @param [Symbol] method_name Method to invoke
172
+ # @param [Array<Object>] args Method arguments
173
+ # @yield Deferred block forwarded to the wrapped logger
174
+ # @return [Object] The delegated result
175
+ def method_missing(method_name, *args, &block)
176
+ return super unless @logger.respond_to?(method_name)
177
+
178
+ @logger.public_send(method_name, *args, &block)
179
+ end
180
+
181
+ private
182
+
183
+ # Dispatch a severity-specific log call after sanitization.
184
+ #
185
+ # @param [Symbol] level Logger method name
186
+ # @param [Object, nil] progname Optional program name
187
+ # @yieldreturn [Object] Deferred log message
188
+ # @return [Object] The underlying logger result
189
+ def log(level, progname = nil)
190
+ if block_given?
191
+ @logger.public_send(level, sanitize(progname)) { sanitize(yield) }
192
+ else
193
+ @logger.public_send(level, sanitize(progname))
194
+ end
195
+ end
196
+
197
+ # Sanitize a logger message when it is a String.
198
+ #
199
+ # @param [Object] message Potential logger payload
200
+ # @return [Object] Unchanged non-String payloads, sanitized String payloads
201
+ def sanitize(message)
202
+ return message unless message.is_a?(String)
203
+
204
+ sanitized = message.dup
205
+ sanitized = sanitize_authorization_header(sanitized)
206
+ sanitized = sanitize_json_pairs(sanitized)
207
+ sanitize_form_and_query_pairs(sanitized)
208
+ end
209
+
210
+ # The initialized thing filter used by this logger.
211
+ #
212
+ # This is a per-logger snapshot created during initialization.
213
+ #
214
+ # @return [ThingFilter]
215
+ attr_reader :thing_filter
216
+
217
+ # Redact Authorization header values.
218
+ #
219
+ # @param [String] message Logger message
220
+ # @return [String] Sanitized logger message
221
+ def sanitize_authorization_header(message)
222
+ message.gsub(/(Authorization:\s*)(?:\"[^\"]*\"|[^\r\n]+)/i, "\\1\"#{thing_filter.label}\"")
223
+ end
224
+
225
+ # Redact JSON-style values for configured sensitive key names.
226
+ #
227
+ # @param [String] message Logger message
228
+ # @return [String] Sanitized logger message
229
+ def sanitize_json_pairs(message)
230
+ message.gsub(/([\"'])(#{thing_filter.pattern_source})\1(\s*:\s*)([\"'])(.*?)\4/i) do
231
+ %(#{$1}#{$2}#{$1}#{$3}#{$4}#{thing_filter.label}#{$4})
232
+ end
233
+ end
234
+
235
+ # Redact form-encoded and query-string values for configured sensitive key names.
236
+ #
237
+ # @param [String] message Logger message
238
+ # @return [String] Sanitized logger message
239
+ def sanitize_form_and_query_pairs(message)
240
+ message.gsub(/(\b(?:#{thing_filter.pattern_source})=)([^&\s\"]+)/i, "\\1#{thing_filter.label}")
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module Sanitizer
5
+ # Small value object for matching and filtering named things.
6
+ #
7
+ # Used by multiple filtering surfaces in the library, such as inspected
8
+ # object attributes and debug-log key filtering.
9
+ #
10
+ # `ThingFilter` snapshots and duplicates its inputs on initialization so later
11
+ # mutation of caller-owned arrays or strings does not affect existing filter
12
+ # instances.
13
+ class ThingFilter
14
+ # Create a new filter.
15
+ #
16
+ # @param [Enumerable<#to_s>] things Names that should be filtered
17
+ # @param [String] label Replacement label to use for filtered values
18
+ #
19
+ # The provided values are duplicated and frozen so the filter remains
20
+ # stable for the lifetime of the object.
21
+ def initialize(things, label:)
22
+ @things = Array(things).map { |thing| thing.to_s.dup.freeze }.freeze
23
+ @label = label.to_s.dup.freeze
24
+ @pattern_source = Regexp.union(@things).source.freeze
25
+ end
26
+
27
+ # The configured names that should be filtered.
28
+ #
29
+ # @return [Array<String>]
30
+ attr_reader :things
31
+
32
+ # The configured replacement label.
33
+ #
34
+ # @return [String]
35
+ attr_reader :label
36
+
37
+ # True when the provided name matches any configured filter entry.
38
+ #
39
+ # Matching is substring-based so it works naturally with instance-variable
40
+ # names used by `#inspect`, such as `@secret` matching `secret`.
41
+ #
42
+ # @param [#to_s] thing_name Candidate thing name
43
+ # @return [Boolean]
44
+ def filtered?(thing_name)
45
+ thing_name_str = thing_name.to_s
46
+ things.any? { |thing| thing_name_str.include?(thing) }
47
+ end
48
+
49
+ # Build a regular-expression source for the configured thing names.
50
+ #
51
+ # Useful when a filtering surface needs regex-based replacement rather than
52
+ # direct name checks.
53
+ #
54
+ # @return [String]
55
+ attr_reader :pattern_source
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module Sanitizer
5
+ module Version
6
+ VERSION = "0.1.0"
7
+ end
8
+ VERSION = Version::VERSION # Traditional Constant Location
9
+ end
10
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "version_gem"
4
+ require_relative "sanitizer/version"
5
+ require_relative "sanitizer/thing_filter"
6
+ require_relative "sanitizer/filtered_attributes"
7
+ require_relative "sanitizer/sanitized_logger"
8
+
9
+ Auth::Sanitizer::Version.class_eval do
10
+ extend VersionGem::Basic
11
+ end
12
+
13
+ module Auth
14
+ module Sanitizer
15
+ class Error < StandardError; end
16
+
17
+ # Default keys filtered from debug log output.
18
+ DEFAULT_FILTERED_KEYS = %w[
19
+ access_token
20
+ refresh_token
21
+ id_token
22
+ client_secret
23
+ assertion
24
+ code_verifier
25
+ token
26
+ ].freeze
27
+
28
+ # Default replacement label for redacted values.
29
+ DEFAULT_FILTERED_LABEL = "[FILTERED]"
30
+
31
+ # Default callable used to provide the filtered replacement label.
32
+ DEFAULT_FILTERED_LABEL_PROVIDER = -> { DEFAULT_FILTERED_LABEL }
33
+
34
+ filtered_label_provider = DEFAULT_FILTERED_LABEL_PROVIDER
35
+ filtered_label_provider_mutex = Mutex.new
36
+
37
+ # Returns the current filtered label by calling the installed provider.
38
+ #
39
+ # Host gems may install a provider that reads from their own config by
40
+ # calling {filtered_label_provider=}.
41
+ #
42
+ # @return [String]
43
+ define_singleton_method(:filtered_label) do
44
+ filtered_label_provider_mutex.synchronize { filtered_label_provider }.call
45
+ end
46
+
47
+ # Install a custom provider for the filtered label.
48
+ #
49
+ # The provider is called each time a new {FilteredAttributes}- or
50
+ # {SanitizedLogger}-bearing object is initialized, allowing the label to
51
+ # track a host gem's live configuration while still being snapshotted per
52
+ # object instance.
53
+ #
54
+ # @example Delegate to a host gem's config
55
+ # Auth::Sanitizer.filtered_label_provider = -> { MyGem.config[:filtered_label] }
56
+ #
57
+ # @param [#call] provider A callable that returns the label string
58
+ # @return [void]
59
+ define_singleton_method(:filtered_label_provider=) do |provider|
60
+ filtered_label_provider_mutex.synchronize do
61
+ filtered_label_provider = provider
62
+ end
63
+ end
64
+
65
+ class << self
66
+ # Returns the default set of key names filtered from debug log output.
67
+ #
68
+ # Host gems may override this by passing `filtered_keys:` directly to
69
+ # {SanitizedLogger#initialize}.
70
+ #
71
+ # @return [Array<String>]
72
+ def default_filtered_keys
73
+ DEFAULT_FILTERED_KEYS
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,6 @@
1
+ module Auth
2
+ module Sanitizer
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
data.tar.gz.sig ADDED
@@ -0,0 +1 @@
1
+ rlV�w����|�S6���[={ۿ��-Y��^�rv�ql�pT�ګ����k��b��%j�k�U� LY��NF�'��N(�my/�Z�����U�F�f7/*à�J����n�h H_���3#Н�Xf��� (��}���/f-d��FG�u<��L�&�M ��x2�&V[>6#��ǭf<�kG̲m�k�P�q’�ڴtz&�|�1}@?<�&�ی�/iM�����+��7l�_���D:6��̄����B9�r�Xi+�~�Gl��7A̡^Cޢm[��{�o:J�.�x �|��ȟ��^�gq|]ܤ�E��'ا�0Nh1:#P#dBѨ�9�r���mٴ��l��g��:��L$�B���,AvU���G����"�A_�EGв�6