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
- 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.
{ anonymous: { disabled: false, mask_private_ips: true }.freeze, masked: { disabled: false, mask_private_ips: false }.freeze, audit: { disabled: true }.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
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/otto/privacy/config.rb', line 130 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.
58 59 60 |
# File 'lib/otto/privacy/config.rb', line 58 def anonymizer_db_path @anonymizer_db_path end |
#anonymizer_enabled ⇒ Object
Returns the value of attribute anonymizer_enabled.
56 57 58 |
# File 'lib/otto/privacy/config.rb', line 56 def anonymizer_enabled @anonymizer_enabled end |
#asn_db_path ⇒ Object
Returns the value of attribute asn_db_path.
58 59 60 |
# File 'lib/otto/privacy/config.rb', line 58 def asn_db_path @asn_db_path end |
#asn_enabled ⇒ Object
Returns the value of attribute asn_enabled.
56 57 58 |
# File 'lib/otto/privacy/config.rb', line 56 def asn_enabled @asn_enabled end |
#correlation_secret ⇒ Object
Returns the value of attribute correlation_secret.
58 59 60 |
# File 'lib/otto/privacy/config.rb', line 58 def correlation_secret @correlation_secret end |
#disabled ⇒ Object (readonly)
Returns the value of attribute disabled.
58 59 60 |
# File 'lib/otto/privacy/config.rb', line 58 def disabled @disabled end |
#geo_db_path ⇒ Object
Returns the value of attribute geo_db_path.
58 59 60 |
# File 'lib/otto/privacy/config.rb', line 58 def geo_db_path @geo_db_path end |
#geo_enabled ⇒ Object
Returns the value of attribute geo_enabled.
56 57 58 |
# File 'lib/otto/privacy/config.rb', line 56 def geo_enabled @geo_enabled end |
#geo_header ⇒ Object
Returns the value of attribute geo_header.
58 59 60 |
# File 'lib/otto/privacy/config.rb', line 58 def geo_header @geo_header end |
#hash_rotation_period ⇒ Object
Returns the value of attribute hash_rotation_period.
56 57 58 |
# File 'lib/otto/privacy/config.rb', line 56 def hash_rotation_period @hash_rotation_period end |
#mask_private_ips ⇒ Object
Returns the value of attribute mask_private_ips.
56 57 58 |
# File 'lib/otto/privacy/config.rb', line 56 def mask_private_ips @mask_private_ips end |
#octet_precision ⇒ Object
Returns the value of attribute octet_precision.
56 57 58 |
# File 'lib/otto/privacy/config.rb', line 56 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_*’).
433 434 435 436 437 438 439 440 441 |
# File 'lib/otto/privacy/config.rb', line 433 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.
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/otto/privacy/config.rb', line 378 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
68 69 70 71 |
# File 'lib/otto/privacy/config.rb', line 68 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.
314 315 316 |
# File 'lib/otto/privacy/config.rb', line 314 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=.
303 304 305 306 307 308 309 |
# File 'lib/otto/privacy/config.rb', line 303 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.
294 295 296 |
# File 'lib/otto/privacy/config.rb', line 294 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=.
283 284 285 286 287 288 289 |
# File 'lib/otto/privacy/config.rb', line 283 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.
464 465 466 467 |
# File 'lib/otto/privacy/config.rb', line 464 def disable! @disabled = true self end |
#disabled? ⇒ Boolean
Check if privacy is disabled
453 454 455 |
# File 'lib/otto/privacy/config.rb', line 453 def disabled? @disabled end |
#enable! ⇒ self
Enable privacy (default state)
472 473 474 475 |
# File 'lib/otto/privacy/config.rb', line 472 def enable! @disabled = false self end |
#enabled? ⇒ Boolean
Check if privacy is enabled
446 447 448 |
# File 'lib/otto/privacy/config.rb', line 446 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.
275 276 277 |
# File 'lib/otto/privacy/config.rb', line 275 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!.
252 253 254 255 256 257 258 |
# File 'lib/otto/privacy/config.rb', line 252 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!.
361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/otto/privacy/config.rb', line 361 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.
345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/otto/privacy/config.rb', line 345 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.
328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/otto/privacy/config.rb', line 328 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.
422 423 424 425 426 427 |
# File 'lib/otto/privacy/config.rb', line 422 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.
Sets only the knobs the profile names (see PROFILES); other settings (octet_precision, geo, correlation_secret, …) are untouched.
Presets are applied, not reset: a knob a profile does not name keeps its
previous value. Switching :anonymous -> :audit therefore leaves
mask_private_ips true, because :audit names only disabled. That is
inert rather than wrong — disabled short-circuits privacy_enabled?
before mask_private_ips is ever read, and #profile below tests @disabled
first, so the derived label stays accurate. Switching on to :masked
re-sets both knobs explicitly. Only surprising if you read the raw ivars.
409 410 411 412 413 |
# File 'lib/otto/privacy/config.rb', line 409 def profile=(profile) presets = self.class.profile_presets(profile) @disabled = presets[:disabled] if presets.key?(:disabled) @mask_private_ips = presets[:mask_private_ips] if presets.key?(:mask_private_ips) 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.
233 234 235 236 237 238 239 240 241 |
# File 'lib/otto/privacy/config.rb', line 233 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:timestamp - Stores the rotation key with TTL
491 492 493 494 495 496 497 |
# File 'lib/otto/privacy/config.rb', line 491 def rotation_key if @redis rotation_key_redis else rotation_key_memory end end |
#validate! ⇒ Object
Validate configuration settings
502 503 504 505 506 507 508 509 510 511 512 513 |
# File 'lib/otto/privacy/config.rb', line 502 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 |