Class: Otto::Privacy::Config
- Inherits:
-
Object
- Object
- Otto::Privacy::Config
- Includes:
- Core::Freezable
- Defined in:
- lib/otto/privacy/config.rb
Overview
Configuration for IP privacy features
Privacy is ENABLED by default for public IPs. Private/localhost IPs are not masked.
rubocop:disable Metrics/ClassLength -- three parallel database configurations (geo, ASN, anonymizer) live here by design: each is a thin, symmetric writer/reader/loader trio, and splitting them into modules would hide the symmetry that makes them reviewable.
Constant Summary collapse
- MAXMIND_DB_REQUIREMENT =
'~> 1.2'- PROFILES =
Named privacy profiles: validated presets over the individual knobs, so a deployment's observability posture is declared in one reviewable word instead of inferred from knob combinations.
- :anonymous — mask every IP, including private/localhost. For deployments where even internal addresses are treated as PII.
- :masked — the default posture: public IPs masked, private and localhost exempt (development-friendly privacy-by-default).
- :audit — privacy disabled: real IPs flow to env and logs. For private/compliance environments where granular attributability supersedes IP privacy; retention responsibility transfers to the operator.
Note the axis this controls: what PERSISTS observably (env keys, logs, fingerprints). Precise ephemeral matching against the unmasked IP does not require :audit — see EnvKeys::IP_MATCH, available in every profile.
This table is the only list of knobs a profile governs: #profile= assigns whatever keys a preset carries, through each knob's writer so any check or normalization the writer does still runs. Every profile names the same keys, so applying one yields the same state whatever preceded it, and each key names a Config attribute with a writer and a public reader.
{ anonymous: { disabled: false, mask_private_ips: true }.freeze, masked: { disabled: false, mask_private_ips: false }.freeze, audit: { disabled: true, mask_private_ips: false }.freeze, }.freeze
Instance Attribute Summary collapse
-
#anonymizer_db_path ⇒ Object
Returns the value of attribute anonymizer_db_path.
-
#anonymizer_enabled ⇒ Object
Returns the value of attribute anonymizer_enabled.
-
#asn_db_path ⇒ Object
Returns the value of attribute asn_db_path.
-
#asn_enabled ⇒ Object
Returns the value of attribute asn_enabled.
-
#correlation_secret ⇒ Object
Returns the value of attribute correlation_secret.
-
#disabled ⇒ Object
readonly
Returns the value of attribute disabled.
-
#geo_db_path ⇒ Object
Returns the value of attribute geo_db_path.
-
#geo_enabled ⇒ Object
Returns the value of attribute geo_enabled.
-
#geo_header ⇒ Object
Returns the value of attribute geo_header.
-
#hash_rotation_period ⇒ Object
Returns the value of attribute hash_rotation_period.
-
#mask_private_ips ⇒ Object
Returns the value of attribute mask_private_ips.
-
#octet_precision ⇒ Object
Returns the value of attribute octet_precision.
Class Method Summary collapse
-
.canonicalize_geo_header(value) ⇒ String?
Canonicalize a geo header name to a Rack CGI env key ('HTTP_*').
-
.profile_presets(profile) ⇒ Hash
Look up the preset hash for a named profile, failing fast on typos.
-
.rotation_keys_store ⇒ Concurrent::Map
Get the class-level rotation keys store.
Instance Method Summary collapse
-
#anonymizer_db_reader ⇒ #get?
The effective anonymizer reader, or nil when classification is off.
-
#anonymizer_db_reader=(reader) ⇒ Object
Inject a ready-made MMDB reader for anonymizer lookups.
-
#asn_db_reader ⇒ #get?
The effective ASN reader, or nil when ASN resolution is off.
-
#asn_db_reader=(reader) ⇒ Object
Inject a ready-made MMDB reader for ASN lookups.
-
#disable! ⇒ self
Disable privacy (allows access to original IPs).
-
#disabled? ⇒ Boolean
Check if privacy is disabled.
-
#enable! ⇒ self
Enable privacy (default state).
-
#enabled? ⇒ Boolean
Check if privacy is enabled.
-
#geo_db_reader ⇒ #get?
The effective MMDB reader for this config, or nil.
-
#geo_db_reader=(reader) ⇒ Object
Inject a ready-made MMDB reader (any object responding to #get).
-
#initialize(options = {}) ⇒ Config
constructor
Initialize privacy configuration.
-
#load_anonymizer_database! ⇒ void
Build/attach the anonymizer database reader.
-
#load_asn_database! ⇒ void
Build/attach the ASN database reader.
-
#load_geo_database! ⇒ void
Build/attach the geo database reader for the current configuration.
-
#profile ⇒ Symbol
The profile the current knob state corresponds to.
-
#profile=(profile) ⇒ Object
Apply a named privacy profile's presets to this config.
-
#replace_enrichment_database_path!(prefix, value) ⇒ void
private
Replace one enrichment database path only after its new reader has opened successfully.
-
#rotation_key ⇒ String
Get the current rotation key for IP hashing.
-
#validate! ⇒ Object
Validate configuration settings.
Methods included from Core::Freezable
Constructor Details
#initialize(options = {}) ⇒ Config
Initialize privacy configuration
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/otto/privacy/config.rb', line 140 def initialize( = {}) = self.class.profile_presets([:profile]).merge() unless [:profile].nil? @octet_precision = .fetch(:octet_precision, 1) @hash_rotation_period = .fetch(:hash_rotation_period, 86_400) # 24 hours @geo_enabled = .fetch(:geo_enabled, true) @disabled = .fetch(:disabled, false) # Enabled by default (privacy-by-default) @mask_private_ips = .fetch(:mask_private_ips, false) # Don't mask private/localhost by default self.correlation_secret = .fetch(:correlation_secret, nil) # Opt-in stable IP-correlation secret @redis = [:redis] # Optional Redis connection for multi-server environments # Geo-location fallback configuration (all opt-in, boot-time only). @geo_db_reader = nil # effective MMDB reader (built from path or injected) @geo_db_override = nil # reader injected via geo_db_reader= (wins over path) self.geo_header = [:geo_header] # canonicalized to an HTTP_* env key (or nil) self.geo_db_reader = [:geo_db_reader] if .key?(:geo_db_reader) @geo_db_path = normalize_db_path([:geo_db_path]) load_geo_database! # build/attach the reader now so a bad path fails at boot # ASN enrichment (opt-in, boot-time only). Same two-ivar shape as geo. @asn_enabled = .fetch(:asn_enabled, false) @asn_db_reader = nil @asn_db_override = nil self.asn_db_reader = [:asn_db_reader] if .key?(:asn_db_reader) @asn_db_path = normalize_db_path([:asn_db_path]) load_asn_database! # Anonymizer classification (opt-in, boot-time only). @anonymizer_enabled = .fetch(:anonymizer_enabled, false) @anonymizer_db_reader = nil @anonymizer_db_override = nil self.anonymizer_db_reader = [:anonymizer_db_reader] if .key?(:anonymizer_db_reader) @anonymizer_db_path = normalize_db_path([:anonymizer_db_path]) load_anonymizer_database! end |
Instance Attribute Details
#anonymizer_db_path ⇒ Object
Returns the value of attribute anonymizer_db_path.
68 69 70 |
# File 'lib/otto/privacy/config.rb', line 68 def anonymizer_db_path @anonymizer_db_path end |
#anonymizer_enabled ⇒ Object
Returns the value of attribute anonymizer_enabled.
66 67 68 |
# File 'lib/otto/privacy/config.rb', line 66 def anonymizer_enabled @anonymizer_enabled end |
#asn_db_path ⇒ Object
Returns the value of attribute asn_db_path.
68 69 70 |
# File 'lib/otto/privacy/config.rb', line 68 def asn_db_path @asn_db_path end |
#asn_enabled ⇒ Object
Returns the value of attribute asn_enabled.
66 67 68 |
# File 'lib/otto/privacy/config.rb', line 66 def asn_enabled @asn_enabled end |
#correlation_secret ⇒ Object
Returns the value of attribute correlation_secret.
68 69 70 |
# File 'lib/otto/privacy/config.rb', line 68 def correlation_secret @correlation_secret end |
#disabled ⇒ Object
Returns the value of attribute disabled.
68 69 70 |
# File 'lib/otto/privacy/config.rb', line 68 def disabled @disabled end |
#geo_db_path ⇒ Object
Returns the value of attribute geo_db_path.
68 69 70 |
# File 'lib/otto/privacy/config.rb', line 68 def geo_db_path @geo_db_path end |
#geo_enabled ⇒ Object
Returns the value of attribute geo_enabled.
66 67 68 |
# File 'lib/otto/privacy/config.rb', line 66 def geo_enabled @geo_enabled end |
#geo_header ⇒ Object
Returns the value of attribute geo_header.
68 69 70 |
# File 'lib/otto/privacy/config.rb', line 68 def geo_header @geo_header end |
#hash_rotation_period ⇒ Object
Returns the value of attribute hash_rotation_period.
66 67 68 |
# File 'lib/otto/privacy/config.rb', line 66 def hash_rotation_period @hash_rotation_period end |
#mask_private_ips ⇒ Object
Returns the value of attribute mask_private_ips.
66 67 68 |
# File 'lib/otto/privacy/config.rb', line 66 def mask_private_ips @mask_private_ips end |
#octet_precision ⇒ Object
Returns the value of attribute octet_precision.
66 67 68 |
# File 'lib/otto/privacy/config.rb', line 66 def octet_precision @octet_precision end |
Class Method Details
.canonicalize_geo_header(value) ⇒ String?
Canonicalize a geo header name to a Rack CGI env key ('HTTP_*').
438 439 440 441 442 443 444 445 446 |
# File 'lib/otto/privacy/config.rb', line 438 def self.canonicalize_geo_header(value) return nil if value.nil? key = value.to_s.strip return nil if key.empty? key = key.upcase.tr('-', '_') key.start_with?('HTTP_') ? key : "HTTP_#{key}" end |
.profile_presets(profile) ⇒ Hash
Look up the preset hash for a named profile, failing fast on typos.
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/otto/privacy/config.rb', line 388 def self.profile_presets(profile) # Neither Integer nor NilClass responds to #to_sym, so an unguarded # conversion raises NoMethodError for `profile: 123` or an explicit # `profile: nil` — an opaque failure inconsistent with the ArgumentError # the rest of this class raises for bad input (cf. correlation_secret=). unless profile.respond_to?(:to_sym) raise ArgumentError, "Privacy profile must be a Symbol or String, got: #{profile.class}" end PROFILES.fetch(profile.to_sym) do raise ArgumentError, "Unknown privacy profile: #{profile.inspect} (valid: #{PROFILES.keys.join(', ')})" end end |
.rotation_keys_store ⇒ Concurrent::Map
Get the class-level rotation keys store
78 79 80 81 |
# File 'lib/otto/privacy/config.rb', line 78 def rotation_keys_store @rotation_keys_store = Concurrent::Map.new unless defined?(@rotation_keys_store) && @rotation_keys_store @rotation_keys_store end |
Instance Method Details
#anonymizer_db_reader ⇒ #get?
The effective anonymizer reader, or nil when classification is off.
324 325 326 |
# File 'lib/otto/privacy/config.rb', line 324 def anonymizer_db_reader @anonymizer_enabled ? @anonymizer_db_reader : nil end |
#anonymizer_db_reader=(reader) ⇒ Object
Inject a ready-made MMDB reader for anonymizer lookups. See #geo_db_reader=.
313 314 315 316 317 318 319 |
# File 'lib/otto/privacy/config.rb', line 313 def anonymizer_db_reader=(reader) unless reader.nil? || reader.respond_to?(:get) raise ArgumentError, "anonymizer_db_reader must respond to :get, got: #{reader.class}" end @anonymizer_db_override = reader end |
#asn_db_reader ⇒ #get?
The effective ASN reader, or nil when ASN resolution is off.
304 305 306 |
# File 'lib/otto/privacy/config.rb', line 304 def asn_db_reader @asn_enabled ? @asn_db_reader : nil end |
#asn_db_reader=(reader) ⇒ Object
Inject a ready-made MMDB reader for ASN lookups. See #geo_db_reader=.
293 294 295 296 297 298 299 |
# File 'lib/otto/privacy/config.rb', line 293 def asn_db_reader=(reader) unless reader.nil? || reader.respond_to?(:get) raise ArgumentError, "asn_db_reader must respond to :get, got: #{reader.class}" end @asn_db_override = reader end |
#disable! ⇒ self
Disable privacy (allows access to original IPs)
IMPORTANT: This should only be used when you have a specific requirement to access original IP addresses. By default, Otto provides privacy-safe masked IPs.
469 470 471 472 |
# File 'lib/otto/privacy/config.rb', line 469 def disable! @disabled = true self end |
#disabled? ⇒ Boolean
Check if privacy is disabled
458 459 460 |
# File 'lib/otto/privacy/config.rb', line 458 def disabled? @disabled end |
#enable! ⇒ self
Enable privacy (default state)
477 478 479 480 |
# File 'lib/otto/privacy/config.rb', line 477 def enable! @disabled = false self end |
#enabled? ⇒ Boolean
Check if privacy is enabled
451 452 453 |
# File 'lib/otto/privacy/config.rb', line 451 def enabled? !@disabled end |
#geo_db_reader ⇒ #get?
The effective MMDB reader for this config, or nil.
Returns nil when geo is disabled (so geo: false consults no database
even if one was previously configured) or when neither a reader override
nor a database path is set.
The reader is a plain instance variable — no class-level store. A MaxMind::DB reader computes its IPv4 start node eagerly at construction and performs no instance mutation on #get, so it is thread-safe under concurrency and unaffected by the shallow freeze deep_freeze! applies to it. That removes the only reason to hold it off-instance, and avoids an unbounded, never-evicted process-lifetime cache — important for a long-running server.
285 286 287 |
# File 'lib/otto/privacy/config.rb', line 285 def geo_db_reader @geo_enabled ? @geo_db_reader : nil end |
#geo_db_reader=(reader) ⇒ Object
Inject a ready-made MMDB reader (any object responding to #get).
This keeps the reader choice independent of Otto (MaxMind::DB, yhirose's maxminddb, or a custom object all work) and is the seam used by tests. When set, it takes precedence over #geo_db_path. Passing nil clears the override. Takes effect on the next #load_geo_database!.
262 263 264 265 266 267 268 |
# File 'lib/otto/privacy/config.rb', line 262 def geo_db_reader=(reader) unless reader.nil? || reader.respond_to?(:get) raise ArgumentError, "geo_db_reader must respond to :get, got: #{reader.class}" end @geo_db_override = reader end |
#load_anonymizer_database! ⇒ void
This method returns an undefined value.
Build/attach the anonymizer database reader. See #load_geo_database!.
371 372 373 374 375 376 377 378 379 380 381 |
# File 'lib/otto/privacy/config.rb', line 371 def load_anonymizer_database! @anonymizer_db_reader = nil return unless @anonymizer_enabled @anonymizer_db_reader = if @anonymizer_db_override @anonymizer_db_override elsif @anonymizer_db_path build_maxmind_reader(@anonymizer_db_path, option_name: 'anonymizer_db_path') end end |
#load_asn_database! ⇒ void
This method returns an undefined value.
Build/attach the ASN database reader. See #load_geo_database! — same boot-time contract, same override-wins-over-path resolution.
355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/otto/privacy/config.rb', line 355 def load_asn_database! @asn_db_reader = nil return unless @asn_enabled @asn_db_reader = if @asn_db_override @asn_db_override elsif @asn_db_path build_maxmind_reader(@asn_db_path, option_name: 'asn_db_path') end end |
#load_geo_database! ⇒ void
This method returns an undefined value.
Build/attach the geo database reader for the current configuration.
Boot-time only. Resolves the effective reader (injected override wins over a path). A String path is opened eagerly here so an unreadable path or a missing 'maxmind-db' gem raises now, at configuration time, rather than on the first request that needs a lookup. When geo is disabled, no database is loaded and no reader is retained.
338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/otto/privacy/config.rb', line 338 def load_geo_database! @geo_db_reader = nil return unless @geo_enabled @geo_db_reader = if @geo_db_override @geo_db_override elsif @geo_db_path build_maxmind_reader(@geo_db_path) end end |
#profile ⇒ Symbol
The profile the current knob state corresponds to.
Derived from the live settings rather than remembering the last
profile= call, so manual knob changes can never leave a stale label:
what this returns is always what the config actually does.
427 428 429 430 431 432 |
# File 'lib/otto/privacy/config.rb', line 427 def profile return :audit if @disabled return :anonymous if @mask_private_ips :masked end |
#profile=(profile) ⇒ Object
Apply a named privacy profile's presets to this config.
Assigns every knob the preset names (see PROFILES), so the result never depends on the profile applied before it: :anonymous -> :audit -> enable! lands on :masked, exactly as Config.new(profile: :audit).enable! does. Other settings (octet_precision, geo, correlation_secret, ...) are untouched.
414 415 416 417 418 |
# File 'lib/otto/privacy/config.rb', line 414 def profile=(profile) self.class.profile_presets(profile).each do |knob, value| send(:"#{knob}=", value) end end |
#replace_enrichment_database_path!(prefix, value) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Replace one enrichment database path only after its new reader has opened successfully. Used by the boot-time configuration path; direct callers should normally use Otto#configure_ip_privacy.
243 244 245 246 247 248 249 250 251 |
# File 'lib/otto/privacy/config.rb', line 243 def replace_enrichment_database_path!(prefix, value) path = normalize_db_path(value) enabled = instance_variable_get(:"@#{prefix}_enabled") reader = path && enabled ? build_maxmind_reader(path, option_name: "#{prefix}_db_path") : nil instance_variable_set(:"@#{prefix}_db_path", path) instance_variable_set(:"@#{prefix}_db_override", nil) instance_variable_set(:"@#{prefix}_db_reader", reader) end |
#rotation_key ⇒ String
Get the current rotation key for IP hashing
Keys rotate at fixed intervals based on hash_rotation_period (default: 24 hours). Each rotation period gets a unique key, ensuring IP addresses hash differently across periods while remaining consistent within.
Multi-server support:
- With Redis: Uses SET NX GET EX for atomic key generation across all servers
- Without Redis: Falls back to in-memory Concurrent::Hash (single-server only)
Redis keys:
- rotation_key:{} - Stores the rotation key with TTL
496 497 498 499 500 501 502 |
# File 'lib/otto/privacy/config.rb', line 496 def rotation_key if @redis rotation_key_redis else rotation_key_memory end end |
#validate! ⇒ Object
Validate configuration settings
507 508 509 510 511 512 513 514 515 516 517 518 |
# File 'lib/otto/privacy/config.rb', line 507 def validate! raise ArgumentError, "octet_precision must be 1 or 2, got: #{@octet_precision}" unless [1, 2].include?(@octet_precision) # Type check before the numeric comparison: a non-Numeric value (false, # a String from unparsed config, ...) would otherwise surface as # NoMethodError/ArgumentError from #<, not a clear configuration error. return if @hash_rotation_period.is_a?(Numeric) && @hash_rotation_period >= 60 raise ArgumentError, "hash_rotation_period must be at least 60 seconds, got: #{@hash_rotation_period.inspect}" end |