familia 2.0.0.pre5 → 2.0.0.pre7

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.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,264 @@
1
+ # lib/familia/features/relationships/permission_management.rb
2
+
3
+ module Familia
4
+ module Features
5
+ module Relationships
6
+ # Permission management module for object-level access control
7
+ #
8
+ # Provides methods for granting, revoking, and checking permissions on objects
9
+ # using the bit-encoded permission system from ScoreEncoding.
10
+ #
11
+ # Usage:
12
+ # class Document < Familia::Horreum
13
+ # include Familia::Features::Relationships::PermissionManagement
14
+ # permission_tracking :user_permissions
15
+ # end
16
+ #
17
+ # doc.grant(user, :read, :write)
18
+ # doc.can?(user, :read) #=> true
19
+ # doc.revoke(user, :write)
20
+ # doc.permissions_for(user) #=> [:read]
21
+ module PermissionManagement
22
+ def self.included(base)
23
+ base.extend(ClassMethods)
24
+ end
25
+
26
+ # Relationships::ClassMethods
27
+ #
28
+ module ClassMethods
29
+ # Enable permission tracking for this class
30
+ #
31
+ # @param field_name [Symbol] Name of the hash field to store permissions
32
+ #
33
+ # @example
34
+ # class Document < Familia::Horreum
35
+ # permission_tracking :user_permissions
36
+ # end
37
+ def permission_tracking(field_name = :permissions)
38
+ # Define a hashkey for storing per-user permissions
39
+ hashkey field_name
40
+
41
+ # Grant permissions to a user for this object
42
+ #
43
+ # @param user [Object] User or user identifier
44
+ # @param permissions [Array<Symbol>] Permissions to grant
45
+ #
46
+ # @example
47
+ # document.grant(user, :read, :write, :edit)
48
+ define_method :grant do |user, *permissions|
49
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
50
+
51
+ # Get current score from any sorted set this object belongs to
52
+ # For simplicity, we'll create a new timestamp-based score
53
+ current_time = Time.now
54
+ new_score = ScoreEncoding.encode_score(current_time, permissions)
55
+
56
+ # Store permission bits in hash for quick lookup
57
+ decoded = ScoreEncoding.decode_score(new_score)
58
+ send(field_name)[user_key] = decoded[:permissions]
59
+ end
60
+
61
+ # Revoke permissions from a user for this object
62
+ #
63
+ # @param user [Object] User or user identifier
64
+ # @param permissions [Array<Symbol>] Permissions to revoke
65
+ #
66
+ # @example
67
+ # document.revoke(user, :write, :edit)
68
+ define_method :revoke do |user, *permissions|
69
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
70
+ current_bits = send(field_name)[user_key].to_i
71
+
72
+ # Remove the specified permission bits
73
+ new_bits = permissions.reduce(current_bits) do |acc, perm|
74
+ acc & ~(ScoreEncoding::PERMISSION_FLAGS[perm] || 0)
75
+ end
76
+
77
+ if new_bits.zero?
78
+ send(field_name).remove_field(user_key)
79
+ else
80
+ send(field_name)[user_key] = new_bits
81
+ end
82
+ end
83
+
84
+ # Check if user has specific permissions for this object
85
+ #
86
+ # @param user [Object] User or user identifier
87
+ # @param permissions [Array<Symbol>] Permissions to check
88
+ # @return [Boolean] True if user has all specified permissions
89
+ #
90
+ # @example
91
+ # document.can?(user, :read, :write) #=> true
92
+ define_method :can? do |user, *permissions|
93
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
94
+ bits = send(field_name)[user_key].to_i
95
+
96
+ permissions.all? do |perm|
97
+ flag = ScoreEncoding::PERMISSION_FLAGS[perm]
98
+ flag && bits.anybits?(flag)
99
+ end
100
+ end
101
+
102
+ # Get all permissions for a user on this object
103
+ #
104
+ # @param user [Object] User or user identifier
105
+ # @return [Array<Symbol>] Array of permission symbols
106
+ #
107
+ # @example
108
+ # document.permissions_for(user) #=> [:read, :write, :edit]
109
+ define_method :permissions_for do |user|
110
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
111
+ bits = send(field_name)[user_key].to_i
112
+ ScoreEncoding.decode_permission_flags(bits)
113
+ end
114
+
115
+ # Add permissions to existing user permissions
116
+ #
117
+ # @param user [Object] User or user identifier
118
+ # @param permissions [Array<Symbol>] Permissions to add
119
+ #
120
+ # @example
121
+ # document.add_permission(user, :delete, :transfer)
122
+ define_method :add_permission do |user, *permissions|
123
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
124
+ current_bits = send(field_name)[user_key].to_i
125
+
126
+ # Add the specified permission bits
127
+ new_bits = permissions.reduce(current_bits) do |acc, perm|
128
+ acc | (ScoreEncoding::PERMISSION_FLAGS[perm] || 0)
129
+ end
130
+
131
+ send(field_name)[user_key] = new_bits
132
+ end
133
+
134
+ # Set exact permissions for a user (replaces existing)
135
+ #
136
+ # @param user [Object] User or user identifier
137
+ # @param permissions [Array<Symbol>] Permissions to set
138
+ #
139
+ # @example
140
+ # document.set_permissions(user, :read, :write)
141
+ define_method :set_permissions do |user, *permissions|
142
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
143
+
144
+ if permissions.empty?
145
+ send(field_name).remove_field(user_key)
146
+ else
147
+ permission_bits = permissions.reduce(0) do |acc, perm|
148
+ acc | (ScoreEncoding::PERMISSION_FLAGS[perm] || 0)
149
+ end
150
+ send(field_name)[user_key] = permission_bits
151
+ end
152
+ end
153
+
154
+ # Get all users and their permissions for this object
155
+ #
156
+ # @return [Hash] Hash mapping user keys to permission arrays
157
+ #
158
+ # @example
159
+ # document.all_permissions
160
+ # #=> { "user123" => [:read, :write], "user456" => [:read] }
161
+ define_method :all_permissions do
162
+ permissions_hash = send(field_name).hgetall
163
+ permissions_hash.transform_values do |bits|
164
+ ScoreEncoding.decode_permission_flags(bits.to_i)
165
+ end
166
+ end
167
+
168
+ # Remove all permissions for all users on this object
169
+ #
170
+ # @example
171
+ # document.clear_all_permissions
172
+ define_method :clear_all_permissions do
173
+ send(field_name).clear
174
+ end
175
+
176
+ # === Two-Stage Filtering Methods ===
177
+
178
+ # Stage 1: Redis pre-filtering via zset membership
179
+ define_method :accessible_items do |collection_key|
180
+ self.class.dbclient.zrange(collection_key, 0, -1, with_scores: true)
181
+ end
182
+
183
+ # Stage 2: Broad categorical filtering on small sets
184
+ define_method :items_by_permission do |collection_key, category = :readable|
185
+ items_with_scores = accessible_items(collection_key)
186
+
187
+ # Operating on ~20-100 items, not millions
188
+ filtered = items_with_scores.select do |(_member, score)|
189
+ ScoreEncoding.category?(score, category)
190
+ end
191
+
192
+ filtered.map(&:first) # Return just the members
193
+ end
194
+
195
+ # Bulk permission check for UI rendering
196
+ define_method :permission_matrix do |collection_key|
197
+ items_with_scores = accessible_items(collection_key)
198
+
199
+ {
200
+ total: items_with_scores.size,
201
+ viewable: items_with_scores.count { |(_, s)| ScoreEncoding.category?(s, :readable) },
202
+ editable: items_with_scores.count { |(_, s)| ScoreEncoding.category?(s, :content_editor) },
203
+ administrative: items_with_scores.count { |(_, s)| ScoreEncoding.category?(s, :administrator) }
204
+ }
205
+ end
206
+
207
+ # Efficient "can perform any administrative action?" check
208
+ # Note: Currently checks if this object has admin privileges in the collection.
209
+ # The user parameter is reserved for future user-specific permission checking.
210
+ define_method :admin_access? do |_user, collection_key|
211
+ score = self.class.dbclient.zscore(collection_key, identifier)
212
+ return false unless score
213
+
214
+ ScoreEncoding.category?(score, :administrator)
215
+ end
216
+
217
+ # === Categorical Permission Methods ===
218
+
219
+ # Check permission category for user
220
+ #
221
+ # @param user [Object] User object to check category for
222
+ # @param category [Symbol] Category to check (:readable, :content_editor, etc.)
223
+ # @return [Boolean] True if user meets the category requirements
224
+ # @example Check if user has content editor permissions
225
+ # document.category?(user, :content_editor) #=> true
226
+ define_method :category? do |user, category|
227
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
228
+ bits = send(field_name)[user_key].to_i
229
+ ScoreEncoding.meets_category?(bits, category)
230
+ end
231
+
232
+ # Get permission tier for user
233
+ #
234
+ # @param user [Object] User object to get tier for
235
+ # @return [Symbol] Permission tier (:administrator, :content_editor, :viewer, :none)
236
+ # @example Get user's permission tier
237
+ # document.permission_tier_for(user) #=> :content_editor
238
+ define_method :permission_tier_for do |user|
239
+ user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
240
+ bits = send(field_name)[user_key].to_i
241
+
242
+ # Create a temporary score to use ScoreEncoding.permission_tier
243
+ temp_score = ScoreEncoding.encode_score(Time.now, bits)
244
+ ScoreEncoding.permission_tier(temp_score)
245
+ end
246
+
247
+ # Get users by permission category
248
+ #
249
+ # @param category [Symbol] Category to filter by
250
+ # @return [Array<String>] Array of user keys with the specified category
251
+ # @example Get all content editors
252
+ # document.users_by_category(:content_editor) #=> ["user123", "user456"]
253
+ define_method :users_by_category do |category|
254
+ permissions_hash = send(field_name).hgetall
255
+ permissions_hash.select do |_user_key, bits|
256
+ ScoreEncoding.meets_category?(bits.to_i, category)
257
+ end.keys
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end