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.
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
-
#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
-
#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_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.
-
#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
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/otto/privacy/config.rb', line 107 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_geo_db_path([:geo_db_path]) load_geo_database! # build/attach the reader now so a bad path fails at boot end |
Instance Attribute Details
#correlation_secret ⇒ Object
Returns the value of attribute correlation_secret.
53 54 55 |
# File 'lib/otto/privacy/config.rb', line 53 def correlation_secret @correlation_secret end |
#disabled ⇒ Object (readonly)
Returns the value of attribute disabled.
53 54 55 |
# File 'lib/otto/privacy/config.rb', line 53 def disabled @disabled end |
#geo_db_path ⇒ Object
Returns the value of attribute geo_db_path.
53 54 55 |
# File 'lib/otto/privacy/config.rb', line 53 def geo_db_path @geo_db_path end |
#geo_enabled ⇒ Object
Returns the value of attribute geo_enabled.
52 53 54 |
# File 'lib/otto/privacy/config.rb', line 52 def geo_enabled @geo_enabled end |
#geo_header ⇒ Object
Returns the value of attribute geo_header.
53 54 55 |
# File 'lib/otto/privacy/config.rb', line 53 def geo_header @geo_header end |
#hash_rotation_period ⇒ Object
Returns the value of attribute hash_rotation_period.
52 53 54 |
# File 'lib/otto/privacy/config.rb', line 52 def hash_rotation_period @hash_rotation_period end |
#mask_private_ips ⇒ Object
Returns the value of attribute mask_private_ips.
52 53 54 |
# File 'lib/otto/privacy/config.rb', line 52 def mask_private_ips @mask_private_ips end |
#octet_precision ⇒ Object
Returns the value of attribute octet_precision.
52 53 54 |
# File 'lib/otto/privacy/config.rb', line 52 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_*’).
287 288 289 290 291 292 293 294 295 |
# File 'lib/otto/privacy/config.rb', line 287 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.
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/otto/privacy/config.rb', line 232 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
62 63 64 65 |
# File 'lib/otto/privacy/config.rb', line 62 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
#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.
318 319 320 321 |
# File 'lib/otto/privacy/config.rb', line 318 def disable! @disabled = true self end |
#disabled? ⇒ Boolean
Check if privacy is disabled
307 308 309 |
# File 'lib/otto/privacy/config.rb', line 307 def disabled? @disabled end |
#enable! ⇒ self
Enable privacy (default state)
326 327 328 329 |
# File 'lib/otto/privacy/config.rb', line 326 def enable! @disabled = false self end |
#enabled? ⇒ Boolean
Check if privacy is enabled
300 301 302 |
# File 'lib/otto/privacy/config.rb', line 300 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.
201 202 203 |
# File 'lib/otto/privacy/config.rb', line 201 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!.
178 179 180 181 182 183 184 |
# File 'lib/otto/privacy/config.rb', line 178 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_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.
215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/otto/privacy/config.rb', line 215 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.
276 277 278 279 280 281 |
# File 'lib/otto/privacy/config.rb', line 276 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.
263 264 265 266 267 |
# File 'lib/otto/privacy/config.rb', line 263 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 |
#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
345 346 347 348 349 350 351 |
# File 'lib/otto/privacy/config.rb', line 345 def rotation_key if @redis rotation_key_redis else rotation_key_memory end end |
#validate! ⇒ Object
Validate configuration settings
356 357 358 359 360 361 362 363 364 365 366 367 |
# File 'lib/otto/privacy/config.rb', line 356 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 |