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.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- 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
|