Module: Familia::Features::ExternalIdentifier
- Defined in:
- lib/familia/features/external_identifier.rb
Overview
Familia::Features::ExternalIdentifier
Defined Under Namespace
Modules: ModelClassMethods, ModelInstanceMethods Classes: ExternalIdentifierError, ExternalIdentifierFieldType
Instance Method Summary collapse
-
#derive_external_identifier ⇒ String?
Derives a deterministic, public-facing external identifier from the object's internal
objid. - #destroy! ⇒ Object
-
#external_identifier ⇒ String
Full-length alias for extid for clarity when needed.
-
#external_identifier=(value) ⇒ Object
Full-length alias setter for extid.
Instance Method Details
#derive_external_identifier ⇒ String?
Derives a deterministic, public-facing external identifier from the object's
internal objid.
This method uses the objid's high-quality randomness to seed a
pseudorandom number generator (PRNG). The PRNG then acts as a complex,
deterministic function to produce a new identifier that has no discernible
mathematical correlation to the objid. This is a security measure to
prevent leaking information (like timestamps from UUIDv7) from the internal
identifier to the public one.
The resulting identifier is always deterministic: the same objid will
always produce the same extid, which is crucial for lookups.
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 |
# File 'lib/familia/features/external_identifier.rb', line 294 def derive_external_identifier raise ExternalIdentifierError, 'Missing objid field' unless respond_to?(:objid) current_objid = objid return nil if current_objid.nil? || current_objid.to_s.empty? # Validate objid provenance for security guarantees validate_objid_provenance! # Normalize the objid to a consistent hex representation first. normalized_hex = normalize_objid_to_hex(current_objid) = self.class.(:external_identifier) # Derive 16 deterministic bytes (128 bits) from the objid. We do not use # SecureRandom because the output must be deterministic (same objid -> # same extid, which lookups depend on). # # We deliberately avoid Random/Mersenne Twister here (see issue #310, # S3): MT is not a cryptographic PRNG -- its internal state can be # reconstructed from observed output -- and the previous implementation # seeded it via `seed.unpack1('Q>')`, discarding all but the first 64 of # the digest's 256 bits. A hash/HMAC keeps the derivation deterministic # while being one-way and preserving the full input entropy. # # When a `secret:` is configured for the feature, a keyed HMAC is used so # external IDs cannot be forged or inverted from a known objid. Without a # secret, a plain SHA-256 still removes the MT weakness and the 64-bit # truncation. secret = [:secret] # Allow a callable secret (e.g. -> { ENV.fetch('EXTID_HMAC_SECRET') }) so # the value resolves lazily at first derivation instead of eagerly at # class-definition time. The model file then loads even when the env var is # absent (CI, tooling, introspection), and a missing secret still raises # loudly here -- where it matters -- rather than masking the whole model # behind a load-time error (#311). secret = secret.call if secret.respond_to?(:call) random_bytes = if secret && !secret.to_s.empty? OpenSSL::HMAC.digest('SHA256', secret.to_s, normalized_hex)[0, 16] else Digest::SHA256.digest(normalized_hex)[0, 16] end # Encode as a base36 string for a compact, URL-safe identifier. # 128 bits is approximately 25 characters in base36. external_part = random_bytes.unpack1('H*').to_i(16).to_s(36).rjust(25, '0') # Get format from feature options and interpolate the ID format = [:format] || 'ext_%{id}' format % { id: external_part } end |
#destroy! ⇒ Object
364 365 366 367 368 369 370 |
# File 'lib/familia/features/external_identifier.rb', line 364 def destroy! # Clean up extid mapping when object is destroyed current_extid = instance_variable_get(:@extid) self.class.extid_lookup.remove_field(current_extid) if current_extid super if defined?(super) end |
#external_identifier ⇒ String
Full-length alias for extid for clarity when needed
352 353 354 |
# File 'lib/familia/features/external_identifier.rb', line 352 def external_identifier extid end |
#external_identifier=(value) ⇒ Object
Full-length alias setter for extid
360 361 362 |
# File 'lib/familia/features/external_identifier.rb', line 360 def external_identifier=(value) self.extid = value end |