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.
{ 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
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 164 165 166 |
# File 'lib/otto/privacy/config.rb', line 133 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.
61 62 63 |
# File 'lib/otto/privacy/config.rb', line 61 def anonymizer_db_path @anonymizer_db_path end |
#anonymizer_enabled ⇒ Object
Returns the value of attribute anonymizer_enabled.
59 60 61 |
# File 'lib/otto/privacy/config.rb', line 59 def anonymizer_enabled @anonymizer_enabled end |
#asn_db_path ⇒ Object
Returns the value of attribute asn_db_path.
61 62 63 |
# File 'lib/otto/privacy/config.rb', line 61 def asn_db_path @asn_db_path end |
#asn_enabled ⇒ Object
Returns the value of attribute asn_enabled.
59 60 61 |
# File 'lib/otto/privacy/config.rb', line 59 def asn_enabled @asn_enabled end |
#correlation_secret ⇒ Object
Returns the value of attribute correlation_secret.
61 62 63 |
# File 'lib/otto/privacy/config.rb', line 61 def correlation_secret @correlation_secret end |
#disabled ⇒ Object (readonly)
Returns the value of attribute disabled.
61 62 63 |
# File 'lib/otto/privacy/config.rb', line 61 def disabled @disabled end |
#geo_db_path ⇒ Object
Returns the value of attribute geo_db_path.
61 62 63 |
# File 'lib/otto/privacy/config.rb', line 61 def geo_db_path @geo_db_path end |
#geo_enabled ⇒ Object
Returns the value of attribute geo_enabled.
59 60 61 |
# File 'lib/otto/privacy/config.rb', line 59 def geo_enabled @geo_enabled end |
#geo_header ⇒ Object
Returns the value of attribute geo_header.
61 62 63 |
# File 'lib/otto/privacy/config.rb', line 61 def geo_header @geo_header end |
#hash_rotation_period ⇒ Object
Returns the value of attribute hash_rotation_period.
59 60 61 |
# File 'lib/otto/privacy/config.rb', line 59 def hash_rotation_period @hash_rotation_period end |
#mask_private_ips ⇒ Object
Returns the value of attribute mask_private_ips.
59 60 61 |
# File 'lib/otto/privacy/config.rb', line 59 def mask_private_ips @mask_private_ips end |
#octet_precision ⇒ Object
Returns the value of attribute octet_precision.
59 60 61 |
# File 'lib/otto/privacy/config.rb', line 59 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_*').
436 437 438 439 440 441 442 443 444 |
# File 'lib/otto/privacy/config.rb', line 436 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.
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 |
# File 'lib/otto/privacy/config.rb', line 381 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
71 72 73 74 |
# File 'lib/otto/privacy/config.rb', line 71 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.
317 318 319 |
# File 'lib/otto/privacy/config.rb', line 317 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=.
306 307 308 309 310 311 312 |
# File 'lib/otto/privacy/config.rb', line 306 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.
297 298 299 |
# File 'lib/otto/privacy/config.rb', line 297 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=.
286 287 288 289 290 291 292 |
# File 'lib/otto/privacy/config.rb', line 286 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.
467 468 469 470 |
# File 'lib/otto/privacy/config.rb', line 467 def disable! @disabled = true self end |
#disabled? ⇒ Boolean
Check if privacy is disabled
456 457 458 |
# File 'lib/otto/privacy/config.rb', line 456 def disabled? @disabled end |
#enable! ⇒ self
Enable privacy (default state)
475 476 477 478 |
# File 'lib/otto/privacy/config.rb', line 475 def enable! @disabled = false self end |
#enabled? ⇒ Boolean
Check if privacy is enabled
449 450 451 |
# File 'lib/otto/privacy/config.rb', line 449 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.
278 279 280 |
# File 'lib/otto/privacy/config.rb', line 278 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!.
255 256 257 258 259 260 261 |
# File 'lib/otto/privacy/config.rb', line 255 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!.
364 365 366 367 368 369 370 371 372 373 374 |
# File 'lib/otto/privacy/config.rb', line 364 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.
348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/otto/privacy/config.rb', line 348 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.
331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/otto/privacy/config.rb', line 331 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.
425 426 427 428 429 430 |
# File 'lib/otto/privacy/config.rb', line 425 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.
412 413 414 415 416 |
# File 'lib/otto/privacy/config.rb', line 412 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.
236 237 238 239 240 241 242 243 244 |
# File 'lib/otto/privacy/config.rb', line 236 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
494 495 496 497 498 499 500 |
# File 'lib/otto/privacy/config.rb', line 494 def rotation_key if @redis rotation_key_redis else rotation_key_memory end end |
#validate! ⇒ Object
Validate configuration settings
505 506 507 508 509 510 511 512 513 514 515 516 |
# File 'lib/otto/privacy/config.rb', line 505 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 |