familia 2.0.0.pre14 → 2.0.0.pre16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +66 -6
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +4 -4
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre13.md +1 -1
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +68 -66
- data/lib/familia/features/expiration/extensions.rb +61 -0
- data/lib/familia/features/expiration.rb +35 -87
- data/lib/familia/features/external_identifier.rb +11 -12
- data/lib/familia/features/object_identifier.rb +58 -20
- data/lib/familia/features/quantization.rb +17 -22
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +69 -271
- data/lib/familia/features/safe_dump.rb +127 -132
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +5 -5
- data/lib/familia/features.rb +21 -21
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -15
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +129 -11
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +77 -45
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -228
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/autoloadable.rb +0 -113
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/autoloadable/autoloadable_try.rb +0 -61
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -5,7 +5,7 @@ require_relative 'encrypted_fields/encrypted_field_type'
|
|
5
5
|
module Familia
|
6
6
|
module Features
|
7
7
|
# EncryptedFields is a feature that provides transparent encryption and decryption
|
8
|
-
# of sensitive data stored in Redis
|
8
|
+
# of sensitive data stored in Valkey/Redis. It uses strong cryptographic algorithms
|
9
9
|
# with field-specific key derivation to protect data at rest while maintaining
|
10
10
|
# easy access patterns for authorized applications.
|
11
11
|
#
|
@@ -46,7 +46,7 @@ module Familia
|
|
46
46
|
# Security Features:
|
47
47
|
#
|
48
48
|
# Each encrypted field uses a unique encryption key derived from:
|
49
|
-
# - Master encryption key (from Familia.
|
49
|
+
# - Master encryption key (from Familia.encryption_keys[current_key_version])
|
50
50
|
# - Field name (cryptographic domain separation)
|
51
51
|
# - Record identifier (per-record key derivation)
|
52
52
|
# - Class name (per-class key derivation)
|
@@ -97,27 +97,22 @@ module Familia
|
|
97
97
|
# # The content can only be decrypted if doc_id, owner_id, and classification
|
98
98
|
# # values match those used during encryption
|
99
99
|
#
|
100
|
-
#
|
100
|
+
# Request-Level Caching:
|
101
101
|
#
|
102
|
-
# For
|
102
|
+
# For performance optimization, enable key derivation caching per request:
|
103
103
|
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
# encrypted_field :diary_entry # Ultra-sensitive
|
109
|
-
# encrypted_field :photos # Ultra-sensitive
|
104
|
+
# Familia::Encryption.with_request_cache do
|
105
|
+
# vault.secret_key = "value1"
|
106
|
+
# vault.api_token = "value2"
|
107
|
+
# vault.save # Reuses derived keys within this block
|
110
108
|
# end
|
111
109
|
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
# # Passphrase required for decryption
|
116
|
-
# diary = vault.diary_entry(passphrase_value: user_passphrase)
|
110
|
+
# # Cache is automatically cleared when block exits
|
111
|
+
# # Or manually: Familia::Encryption.clear_request_cache!
|
117
112
|
#
|
118
|
-
#
|
113
|
+
# Preventing Accidental Leakage:
|
119
114
|
#
|
120
|
-
# Encrypted fields return ConcealedString objects
|
115
|
+
# Encrypted fields return ConcealedString objects to help prevent exposure.
|
121
116
|
#
|
122
117
|
# secret = vault.secret_key
|
123
118
|
# secret.class # => ConcealedString
|
@@ -125,13 +120,11 @@ module Familia
|
|
125
120
|
# secret.inspect # => "[CONCEALED]" (automatic redaction)
|
126
121
|
#
|
127
122
|
# # Safe access pattern
|
128
|
-
# secret.
|
129
|
-
#
|
130
|
-
# api_call(authorization: "Bearer #{value}")
|
131
|
-
# end
|
123
|
+
# raw_value = secret.reveal # Returns actual decrypted string
|
124
|
+
# # Use raw_value carefully - avoid creating copies
|
132
125
|
#
|
133
|
-
# #
|
134
|
-
#
|
126
|
+
# # Check if cleared from memory available to Ruby runtime process.
|
127
|
+
# secret.cleared? # Returns true if wiped
|
135
128
|
#
|
136
129
|
# # Explicit cleanup
|
137
130
|
# secret.clear! # Best-effort memory wiping
|
@@ -143,41 +136,51 @@ module Familia
|
|
143
136
|
# # Invalid ciphertext or tampering
|
144
137
|
# vault.secret_key # => Familia::EncryptionError: Authentication failed
|
145
138
|
#
|
146
|
-
# #
|
147
|
-
#
|
148
|
-
# # => Familia::EncryptionError:
|
139
|
+
# # Missing encryption configuration
|
140
|
+
# Familia.config.encryption_keys = {}
|
141
|
+
# vault.secret_key # => Familia::EncryptionError: No encryption keys configured
|
149
142
|
#
|
150
|
-
# #
|
151
|
-
#
|
152
|
-
# vault.secret_key # => Familia::EncryptionError:
|
143
|
+
# # Invalid key version
|
144
|
+
# # Key exists in storage but not in current configuration
|
145
|
+
# vault.secret_key # => Familia::EncryptionError: Key version not found: v1
|
153
146
|
#
|
154
147
|
# Configuration:
|
155
148
|
#
|
156
|
-
# #
|
149
|
+
# # Configure versioned encryption keys (required)
|
157
150
|
# Familia.configure do |config|
|
158
|
-
# config.
|
159
|
-
#
|
151
|
+
# config.encryption_keys = {
|
152
|
+
# v1: ENV['FAMILIA_ENCRYPTION_KEY'],
|
153
|
+
# v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
|
154
|
+
# }
|
155
|
+
# config.current_key_version = :v2
|
156
|
+
# config.encryption_personalization = 'MyApp-2024' # Optional (XChaCha20 only)
|
160
157
|
# end
|
161
158
|
#
|
162
|
-
# #
|
163
|
-
#
|
164
|
-
# puts key # => "base64-encoded-32-byte-key"
|
159
|
+
# # Validate configuration before use
|
160
|
+
# Familia::Encryption.validate_configuration!
|
165
161
|
#
|
166
162
|
# Key Rotation:
|
167
163
|
#
|
168
164
|
# The feature supports key versioning for seamless key rotation:
|
169
165
|
#
|
170
|
-
# # Step 1: Add new key while keeping old
|
166
|
+
# # Step 1: Add new key version while keeping old keys
|
171
167
|
# Familia.configure do |config|
|
172
|
-
# config.
|
173
|
-
#
|
168
|
+
# config.encryption_keys = {
|
169
|
+
# v1: old_key,
|
170
|
+
# v2: new_key
|
171
|
+
# }
|
172
|
+
# config.current_key_version = :v2
|
174
173
|
# end
|
175
174
|
#
|
176
|
-
# # Step 2: Objects decrypt with
|
177
|
-
# vault.secret_key = "new-secret" # Encrypted with
|
175
|
+
# # Step 2: Objects decrypt with any valid key, encrypt with current key
|
176
|
+
# vault.secret_key = "new-secret" # Encrypted with v2 key
|
177
|
+
# vault.save
|
178
|
+
#
|
179
|
+
# # Step 3: Re-encrypt existing records
|
180
|
+
# vault.re_encrypt_fields! # Uses current key version
|
178
181
|
# vault.save
|
179
182
|
#
|
180
|
-
# # Step
|
183
|
+
# # Step 4: After all data is re-encrypted, remove old key
|
181
184
|
#
|
182
185
|
# Integration Patterns:
|
183
186
|
#
|
@@ -208,10 +211,9 @@ module Familia
|
|
208
211
|
# user = User.find(user_id)
|
209
212
|
#
|
210
213
|
# # Access encrypted field safely
|
211
|
-
# user.credit_card_number.
|
212
|
-
#
|
213
|
-
#
|
214
|
-
# end
|
214
|
+
# cc_number = user.credit_card_number.reveal
|
215
|
+
# # Process payment without storing plaintext
|
216
|
+
# payment_gateway.charge(cc_number, amount)
|
215
217
|
#
|
216
218
|
# # Clear sensitive data from memory
|
217
219
|
# user.credit_card_number.clear!
|
@@ -221,10 +223,12 @@ module Familia
|
|
221
223
|
# Performance Considerations:
|
222
224
|
#
|
223
225
|
# - Encryption/decryption adds ~1-5ms overhead per field
|
224
|
-
# - Key derivation is cached
|
226
|
+
# - Key derivation is NOT cached by default for security
|
227
|
+
# - Use request-level caching for performance: with_request_cache { ... }
|
225
228
|
# - XChaCha20-Poly1305 is ~2x faster than AES-256-GCM
|
226
229
|
# - Memory allocation increases due to ciphertext expansion
|
227
230
|
# - Consider batching operations for high-throughput scenarios
|
231
|
+
# - Personalization only affects XChaCha20-Poly1305 BLAKE2b derivation
|
228
232
|
#
|
229
233
|
# Security Limitations:
|
230
234
|
#
|
@@ -255,15 +259,17 @@ module Familia
|
|
255
259
|
# - Insider threats with application access
|
256
260
|
#
|
257
261
|
module EncryptedFields
|
262
|
+
Familia::Base.add_feature self, :encrypted_fields
|
263
|
+
|
258
264
|
def self.included(base)
|
259
|
-
Familia.trace :LOADED, self, base
|
260
|
-
base.extend
|
265
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
266
|
+
base.extend ModelClassMethods
|
261
267
|
|
262
268
|
# Initialize encrypted fields tracking
|
263
269
|
base.instance_variable_set(:@encrypted_fields, []) unless base.instance_variable_defined?(:@encrypted_fields)
|
264
270
|
end
|
265
271
|
|
266
|
-
module
|
272
|
+
module ModelClassMethods
|
267
273
|
# Define an encrypted field that transparently encrypts/decrypts values
|
268
274
|
#
|
269
275
|
# Encrypted fields are stored as JSON objects containing the encrypted
|
@@ -287,13 +293,13 @@ module Familia
|
|
287
293
|
# encrypted_field :content, aad_fields: [:doc_id, :owner_id]
|
288
294
|
# end
|
289
295
|
#
|
290
|
-
def encrypted_field(name, aad_fields: [], **
|
296
|
+
def encrypted_field(name, aad_fields: [], **)
|
291
297
|
@encrypted_fields ||= []
|
292
298
|
@encrypted_fields << name unless @encrypted_fields.include?(name)
|
293
299
|
|
294
300
|
require_relative 'encrypted_fields/encrypted_field_type'
|
295
301
|
|
296
|
-
field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **
|
302
|
+
field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **)
|
297
303
|
register_field_type(field_type)
|
298
304
|
end
|
299
305
|
|
@@ -324,7 +330,7 @@ module Familia
|
|
324
330
|
algorithm: provider.algorithm_name,
|
325
331
|
key_size: provider.key_size,
|
326
332
|
nonce_size: provider.nonce_size,
|
327
|
-
tag_size: provider.tag_size
|
333
|
+
tag_size: provider.tag_size,
|
328
334
|
}
|
329
335
|
end
|
330
336
|
end
|
@@ -357,9 +363,7 @@ module Familia
|
|
357
363
|
def clear_encrypted_fields!
|
358
364
|
self.class.encrypted_fields.each do |field_name|
|
359
365
|
field_value = instance_variable_get("@#{field_name}")
|
360
|
-
if field_value.respond_to?(:clear!)
|
361
|
-
field_value.clear!
|
362
|
-
end
|
366
|
+
field_value.clear! if field_value.respond_to?(:clear!)
|
363
367
|
end
|
364
368
|
end
|
365
369
|
|
@@ -419,19 +423,17 @@ module Familia
|
|
419
423
|
self.class.encrypted_fields.each_with_object({}) do |field_name, status|
|
420
424
|
field_value = instance_variable_get("@#{field_name}")
|
421
425
|
|
422
|
-
if field_value.nil?
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
426
|
+
status[field_name] = if field_value.nil?
|
427
|
+
{ encrypted: false, value: nil }
|
428
|
+
elsif field_value.respond_to?(:cleared?) && field_value.cleared?
|
429
|
+
{ encrypted: true, cleared: true }
|
430
|
+
elsif field_value.respond_to?(:concealed?) && field_value.concealed?
|
431
|
+
{ encrypted: true, algorithm: 'unknown', cleared: false }
|
432
|
+
else
|
433
|
+
{ encrypted: false, value: '[CONCEALED]' }
|
434
|
+
end
|
431
435
|
end
|
432
436
|
end
|
433
|
-
|
434
|
-
Familia::Base.add_feature self, :encrypted_fields
|
435
437
|
end
|
436
438
|
end
|
437
439
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# lib/familia/features/expiration/extensions.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
# Add a default update_expiration method for all classes that include
|
5
|
+
# Familia::Base. Since expiration is a core feature, we can confidently
|
6
|
+
# call `horreum_instance.update_expiration` without defensive programming
|
7
|
+
# even when expiration is not enabled for the horreum_instance class.
|
8
|
+
module Base
|
9
|
+
# Base implementation of update_expiration that maintains API compatibility
|
10
|
+
# with the :expiration feature's implementation.
|
11
|
+
#
|
12
|
+
# This is a no-op implementation that gets overridden by the :expiration
|
13
|
+
# feature. It accepts an optional default_expiration parameter to maintain
|
14
|
+
# interface compatibility with the overriding implementations.
|
15
|
+
#
|
16
|
+
# @param default_expiration [Numeric, nil] Time To Live in seconds
|
17
|
+
# @return [nil] Always returns nil for the base implementation
|
18
|
+
#
|
19
|
+
# @note This is a no-op implementation. Classes that need expiration
|
20
|
+
# functionality should include the :expiration feature.
|
21
|
+
#
|
22
|
+
# @example Enable expiration feature
|
23
|
+
# class MyModel < Familia::Horreum
|
24
|
+
# feature :expiration
|
25
|
+
# default_expiration 1.hour
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
def update_expiration(default_expiration: nil)
|
29
|
+
Familia.ld <<~LOG
|
30
|
+
[update_expiration] Expiration feature not enabled for #{self.class}.
|
31
|
+
Key: #{dbkey} Arg: #{default_expiration} (caller: #{caller(1..1)})
|
32
|
+
LOG
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Base implementation of ttl that returns -1 (no expiration set)
|
37
|
+
#
|
38
|
+
# @return [Integer] Always returns -1 for the base implementation
|
39
|
+
#
|
40
|
+
def ttl
|
41
|
+
-1
|
42
|
+
end
|
43
|
+
|
44
|
+
# Base implementation of expires? that returns false
|
45
|
+
#
|
46
|
+
# @return [Boolean] Always returns false for the base implementation
|
47
|
+
#
|
48
|
+
def expires?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
# Base implementation of expired? that returns false
|
53
|
+
#
|
54
|
+
# @param _threshold [Numeric] Ignored in base implementation
|
55
|
+
# @return [Boolean] Always returns false for the base implementation
|
56
|
+
#
|
57
|
+
def expired?(_threshold = 0)
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -1,13 +1,15 @@
|
|
1
1
|
# lib/familia/features/expiration.rb
|
2
2
|
|
3
|
+
require_relative 'expiration/extensions'
|
4
|
+
|
3
5
|
module Familia
|
4
6
|
module Features
|
5
7
|
# Expiration is a feature that provides Time To Live (TTL) management for Familia
|
6
|
-
# objects and their associated Redis
|
8
|
+
# objects and their associated Valkey/Redis data structures. It enables automatic
|
7
9
|
# data cleanup and supports cascading expiration across related objects.
|
8
10
|
#
|
9
11
|
# This feature allows you to:
|
10
|
-
# -
|
12
|
+
# - UnsortedSet default expiration times at the class level
|
11
13
|
# - Update expiration times for individual objects
|
12
14
|
# - Cascade expiration settings to related data structures
|
13
15
|
# - Query remaining TTL for objects
|
@@ -33,7 +35,7 @@ module Familia
|
|
33
35
|
# session.update_expiration(30.minutes)
|
34
36
|
# session.ttl # => 1799
|
35
37
|
#
|
36
|
-
# #
|
38
|
+
# # UnsortedSet custom expiration for new objects
|
37
39
|
# session.update_expiration(default_expiration: 2.hours)
|
38
40
|
#
|
39
41
|
# Class-Level Configuration:
|
@@ -141,33 +143,37 @@ module Familia
|
|
141
143
|
#
|
142
144
|
# Performance Considerations:
|
143
145
|
#
|
144
|
-
# - TTL operations are performed on Redis
|
146
|
+
# - TTL operations are performed on Valkey/Redis side with minimal overhead
|
145
147
|
# - Cascading expiration uses pipelining for efficiency when possible
|
146
|
-
# - Zero expiration values skip Redis EXPIRE calls entirely
|
147
|
-
# - TTL queries are direct
|
148
|
+
# - Zero expiration values skip Valkey/Redis EXPIRE calls entirely
|
149
|
+
# - TTL queries are direct db operations (very fast)
|
148
150
|
#
|
149
151
|
module Expiration
|
150
152
|
@default_expiration = nil
|
151
153
|
|
154
|
+
Familia::Base.add_feature self, :expiration
|
155
|
+
|
152
156
|
using Familia::Refinements::TimeLiterals
|
153
157
|
|
154
158
|
def self.included(base)
|
155
|
-
Familia.trace :LOADED, self, base
|
156
|
-
base.extend
|
159
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
160
|
+
base.extend ModelClassMethods
|
157
161
|
|
158
162
|
# Initialize default_expiration instance variable if not already defined
|
159
163
|
# This ensures the class has a place to store its default expiration setting
|
160
164
|
return if base.instance_variable_defined?(:@default_expiration)
|
161
165
|
|
166
|
+
# The instance var here will return the value from the implementing
|
167
|
+
# model class (or nil if it's not set, as you'd expect).
|
162
168
|
base.instance_variable_set(:@default_expiration, @default_expiration)
|
163
169
|
end
|
164
170
|
|
165
|
-
# Familia::Expiration::
|
171
|
+
# Familia::Expiration::ModelClassMethods
|
166
172
|
#
|
167
|
-
module
|
168
|
-
#
|
173
|
+
module ModelClassMethods
|
174
|
+
# UnsortedSet the default expiration time for instances of this class
|
169
175
|
#
|
170
|
-
# @param
|
176
|
+
# @param value [Numeric] Time in seconds (can be fractional)
|
171
177
|
#
|
172
178
|
attr_writer :default_expiration
|
173
179
|
|
@@ -180,7 +186,7 @@ module Familia
|
|
180
186
|
# @param num [Numeric, nil] Expiration time in seconds
|
181
187
|
# @return [Float] The default expiration in seconds
|
182
188
|
#
|
183
|
-
# @example
|
189
|
+
# @example UnsortedSet default expiration
|
184
190
|
# class MyModel < Familia::Horreum
|
185
191
|
# feature :expiration
|
186
192
|
# default_expiration 1.hour
|
@@ -195,7 +201,7 @@ module Familia
|
|
195
201
|
end
|
196
202
|
end
|
197
203
|
|
198
|
-
#
|
204
|
+
# UnsortedSet the default expiration time for this instance
|
199
205
|
#
|
200
206
|
# @param num [Numeric] Expiration time in seconds
|
201
207
|
#
|
@@ -214,9 +220,9 @@ module Familia
|
|
214
220
|
@default_expiration || self.class.default_expiration
|
215
221
|
end
|
216
222
|
|
217
|
-
# Sets an expiration time for the Redis
|
223
|
+
# Sets an expiration time for the Valkey/Redis data associated with this object
|
218
224
|
#
|
219
|
-
# This method allows setting a Time To Live (TTL) for the data in Redis,
|
225
|
+
# This method allows setting a Time To Live (TTL) for the data in Valkey/Redis,
|
220
226
|
# after which it will be automatically removed. The method also handles
|
221
227
|
# cascading expiration to related data structures when applicable.
|
222
228
|
#
|
@@ -262,15 +268,17 @@ module Familia
|
|
262
268
|
# don't want to silently fail at setting expirations and cause data
|
263
269
|
# retention issues (e.g. not removed in a timely fashion).
|
264
270
|
unless default_expiration.is_a?(Numeric)
|
265
|
-
raise Familia::Problem,
|
271
|
+
raise Familia::Problem,
|
272
|
+
"Default expiration must be a number (#{default_expiration.class} given for #{self.class})"
|
266
273
|
end
|
267
274
|
|
268
275
|
unless default_expiration >= 0
|
269
|
-
raise Familia::Problem,
|
276
|
+
raise Familia::Problem,
|
277
|
+
"Default expiration must be non-negative (#{default_expiration} given for #{self.class})"
|
270
278
|
end
|
271
279
|
|
272
280
|
# If zero, simply skip setting an expiry for this key. If we were to set
|
273
|
-
# 0, Redis would drop the key immediately.
|
281
|
+
# 0, Valkey/Redis would drop the key immediately.
|
274
282
|
if default_expiration.zero?
|
275
283
|
Familia.ld "[update_expiration] No expiration for #{self.class} (#{dbkey})"
|
276
284
|
return true
|
@@ -278,8 +286,9 @@ module Familia
|
|
278
286
|
|
279
287
|
Familia.ld "[update_expiration] Expires #{dbkey} in #{default_expiration} seconds"
|
280
288
|
|
281
|
-
# Redis' EXPIRE command returns 1 if the timeout was set, 0
|
282
|
-
# not exist or the timeout could not be set. Via redis-rb,
|
289
|
+
# The Valkey/Redis' EXPIRE command returns 1 if the timeout was set, 0
|
290
|
+
# if key does not exist or the timeout could not be set. Via redis-rb,
|
291
|
+
# it's a boolean.
|
283
292
|
expire(default_expiration)
|
284
293
|
end
|
285
294
|
|
@@ -295,7 +304,7 @@ module Familia
|
|
295
304
|
# expired_session.ttl # => -1
|
296
305
|
#
|
297
306
|
def ttl
|
298
|
-
|
307
|
+
dbclient.ttl(dbkey)
|
299
308
|
end
|
300
309
|
|
301
310
|
# Check if this object's data will expire
|
@@ -303,7 +312,7 @@ module Familia
|
|
303
312
|
# @return [Boolean] true if TTL is set, false if data persists indefinitely
|
304
313
|
#
|
305
314
|
def expires?
|
306
|
-
ttl
|
315
|
+
ttl.positive?
|
307
316
|
end
|
308
317
|
|
309
318
|
# Check if this object's data has expired or will expire soon
|
@@ -321,6 +330,7 @@ module Familia
|
|
321
330
|
current_ttl = ttl
|
322
331
|
return false if current_ttl == -1 # no expiration set
|
323
332
|
return true if current_ttl == -2 # key does not exist
|
333
|
+
|
324
334
|
current_ttl <= threshold
|
325
335
|
end
|
326
336
|
|
@@ -337,7 +347,7 @@ module Familia
|
|
337
347
|
#
|
338
348
|
def extend_expiration(duration)
|
339
349
|
current_ttl = ttl
|
340
|
-
return false
|
350
|
+
return false unless current_ttl.positive? # no current expiration set
|
341
351
|
|
342
352
|
new_ttl = current_ttl + duration.to_f
|
343
353
|
expire(new_ttl)
|
@@ -351,70 +361,8 @@ module Familia
|
|
351
361
|
# session.persist!
|
352
362
|
#
|
353
363
|
def persist!
|
354
|
-
|
364
|
+
dbclient.persist(dbkey)
|
355
365
|
end
|
356
|
-
|
357
|
-
Familia::Base.add_feature self, :expiration
|
358
|
-
end
|
359
|
-
end
|
360
|
-
end
|
361
|
-
|
362
|
-
module Familia
|
363
|
-
# Add a default update_expiration method for all classes that include
|
364
|
-
# Familia::Base. Since expiration is a core feature, we can confidently
|
365
|
-
# call `horreum_instance.update_expiration` without defensive programming
|
366
|
-
# even when expiration is not enabled for the horreum_instance class.
|
367
|
-
module Base
|
368
|
-
# Base implementation of update_expiration that maintains API compatibility
|
369
|
-
# with the :expiration feature's implementation.
|
370
|
-
#
|
371
|
-
# This is a no-op implementation that gets overridden by the :expiration
|
372
|
-
# feature. It accepts an optional default_expiration parameter to maintain
|
373
|
-
# interface compatibility with the overriding implementations.
|
374
|
-
#
|
375
|
-
# @param default_expiration [Numeric, nil] Time To Live in seconds
|
376
|
-
# @return [nil] Always returns nil for the base implementation
|
377
|
-
#
|
378
|
-
# @note This is a no-op implementation. Classes that need expiration
|
379
|
-
# functionality should include the :expiration feature.
|
380
|
-
#
|
381
|
-
# @example Enable expiration feature
|
382
|
-
# class MyModel < Familia::Horreum
|
383
|
-
# feature :expiration
|
384
|
-
# default_expiration 1.hour
|
385
|
-
# end
|
386
|
-
#
|
387
|
-
def update_expiration(default_expiration: nil)
|
388
|
-
Familia.ld <<~LOG
|
389
|
-
[update_expiration] Expiration feature not enabled for #{self.class}.
|
390
|
-
Key: #{dbkey} Arg: #{default_expiration} (caller: #{caller(1..1)})
|
391
|
-
LOG
|
392
|
-
nil
|
393
|
-
end
|
394
|
-
|
395
|
-
# Base implementation of ttl that returns -1 (no expiration set)
|
396
|
-
#
|
397
|
-
# @return [Integer] Always returns -1 for the base implementation
|
398
|
-
#
|
399
|
-
def ttl
|
400
|
-
-1
|
401
|
-
end
|
402
|
-
|
403
|
-
# Base implementation of expires? that returns false
|
404
|
-
#
|
405
|
-
# @return [Boolean] Always returns false for the base implementation
|
406
|
-
#
|
407
|
-
def expires?
|
408
|
-
false
|
409
|
-
end
|
410
|
-
|
411
|
-
# Base implementation of expired? that returns false
|
412
|
-
#
|
413
|
-
# @param threshold [Numeric] Ignored in base implementation
|
414
|
-
# @return [Boolean] Always returns false for the base implementation
|
415
|
-
#
|
416
|
-
def expired?(_threshold = 0)
|
417
|
-
false
|
418
366
|
end
|
419
367
|
end
|
420
368
|
end
|
@@ -5,9 +5,11 @@ module Familia
|
|
5
5
|
# Familia::Features::ExternalIdentifier
|
6
6
|
#
|
7
7
|
module ExternalIdentifier
|
8
|
+
Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
|
9
|
+
|
8
10
|
def self.included(base)
|
9
|
-
Familia.trace :LOADED, self, base
|
10
|
-
base.extend
|
11
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
12
|
+
base.extend ModelClassMethods
|
11
13
|
|
12
14
|
# Ensure default prefix is set in feature options
|
13
15
|
base.add_feature_options(:external_identifier, prefix: 'ext')
|
@@ -41,11 +43,9 @@ module Familia
|
|
41
43
|
# feature :external_identifier
|
42
44
|
# field :email
|
43
45
|
# end
|
44
|
-
#
|
45
46
|
# user = User.new(email: 'user@example.com')
|
46
47
|
# user.objid # => "01234567-89ab-7def-8000-123456789abc"
|
47
48
|
# user.extid # => "ext_abc123def456ghi789" (deterministic from objid)
|
48
|
-
#
|
49
49
|
# # Same objid always produces same extid
|
50
50
|
# user2 = User.new(objid: user.objid, email: 'user@example.com')
|
51
51
|
# user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
|
@@ -75,7 +75,7 @@ module Familia
|
|
75
75
|
|
76
76
|
instance_variable_set(:"@#{field_name}", derived_extid)
|
77
77
|
|
78
|
-
# Update mapping if we have an identifier
|
78
|
+
# Update mapping if we have an identifier (objid)
|
79
79
|
self.class.extid_lookup[derived_extid] = identifier if respond_to?(:identifier) && identifier
|
80
80
|
|
81
81
|
derived_extid
|
@@ -86,7 +86,7 @@ module Familia
|
|
86
86
|
# Override setter to preserve values during initialization
|
87
87
|
#
|
88
88
|
# This ensures that values passed during object initialization
|
89
|
-
# (e.g., when loading from Redis) are preserved and not overwritten
|
89
|
+
# (e.g., when loading from Valkey/Redis) are preserved and not overwritten
|
90
90
|
# by the lazy generation logic.
|
91
91
|
#
|
92
92
|
# @param klass [Class] The class to define the method on
|
@@ -129,9 +129,9 @@ module Familia
|
|
129
129
|
end
|
130
130
|
end
|
131
131
|
|
132
|
-
# ExternalIdentifier::
|
132
|
+
# ExternalIdentifier::ModelClassMethods
|
133
133
|
#
|
134
|
-
module
|
134
|
+
module ModelClassMethods
|
135
135
|
# Find an object by its external identifier
|
136
136
|
#
|
137
137
|
# @param extid [String] The external identifier to search for
|
@@ -142,7 +142,7 @@ module Familia
|
|
142
142
|
|
143
143
|
if Familia.debug?
|
144
144
|
reference = caller(1..1).first
|
145
|
-
Familia.trace :FIND_BY_EXTID,
|
145
|
+
Familia.trace :FIND_BY_EXTID, nil, extid, reference
|
146
146
|
end
|
147
147
|
|
148
148
|
# Look up the primary ID from the external ID mapping
|
@@ -153,7 +153,8 @@ module Familia
|
|
153
153
|
find_by_id(primary_id)
|
154
154
|
rescue Familia::NotFound
|
155
155
|
# If the object was deleted but mapping wasn't cleaned up
|
156
|
-
|
156
|
+
# we could autoclean here, as long as we log it.
|
157
|
+
# extid_lookup.remove_field(extid)
|
157
158
|
nil
|
158
159
|
end
|
159
160
|
end
|
@@ -303,8 +304,6 @@ module Familia
|
|
303
304
|
normalized
|
304
305
|
end
|
305
306
|
end
|
306
|
-
|
307
|
-
Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
|
308
307
|
end
|
309
308
|
end
|
310
309
|
end
|