familia 2.0.0.pre4 → 2.0.0.pre6
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/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -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 +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -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 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/data_types/counter_try.rb +93 -0
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/data_types/lock_try.rb +133 -0
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -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/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +67 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,563 @@
|
|
1
|
+
# RelatableObjects Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
The RelatableObjects feature provides a standardized system for managing object relationships and ownership in Familia applications. It enables objects to have unique identifiers, external references, and ownership relationships while maintaining API versioning and secure object management.
|
6
|
+
|
7
|
+
## Core Concepts
|
8
|
+
|
9
|
+
### Object Identity System
|
10
|
+
|
11
|
+
RelatableObjects introduces a dual-identifier system:
|
12
|
+
|
13
|
+
1. **Object ID (`objid`)**: Internal UUID v7 for system use
|
14
|
+
2. **External ID (`extid`)**: External-facing identifier for API consumers
|
15
|
+
|
16
|
+
### Ownership Model
|
17
|
+
|
18
|
+
Objects can own other objects through a centralized ownership registry:
|
19
|
+
- **Owners**: Objects that possess other objects
|
20
|
+
- **Owned Objects**: Objects that belong to other objects
|
21
|
+
- **Ownership Validation**: Prevents self-ownership and enforces type checking
|
22
|
+
|
23
|
+
## Basic Usage
|
24
|
+
|
25
|
+
### Enabling RelatableObjects
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Customer < Familia::Horreum
|
29
|
+
feature :relatable_object # Note: uses relatable_object, not relatable_objects
|
30
|
+
|
31
|
+
field :name, :email, :plan
|
32
|
+
end
|
33
|
+
|
34
|
+
class Domain < Familia::Horreum
|
35
|
+
feature :relatable_object
|
36
|
+
|
37
|
+
field :name, :dns_zone
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
### Automatic ID Generation
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
customer = Customer.new(name: "Acme Corp", email: "admin@acme.com")
|
45
|
+
|
46
|
+
# IDs are lazily generated on first access
|
47
|
+
customer.objid # => "018c3f8e-7b2a-7f4a-9d8e-1a2b3c4d5e6f" (UUID v7)
|
48
|
+
customer.extid # => "ext_3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z" (54 chars)
|
49
|
+
|
50
|
+
# Alternative accessor methods
|
51
|
+
customer.relatable_objid # Same as objid
|
52
|
+
customer.external_identifier # Same as extid
|
53
|
+
|
54
|
+
# API version tracking
|
55
|
+
customer.api_version # => "v2" (automatically set)
|
56
|
+
```
|
57
|
+
|
58
|
+
### Object Relationships
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# Establish ownership
|
62
|
+
customer = Customer.find_by_objid("018c3f8e-7b2a-7f4a-9d8e-1a2b3c4d5e6f")
|
63
|
+
domain = Domain.new(name: "acme.com")
|
64
|
+
|
65
|
+
# Set ownership (must be implemented by your application)
|
66
|
+
Customer.owners.set(domain.objid, customer.objid)
|
67
|
+
|
68
|
+
# Check ownership
|
69
|
+
domain.owner?(customer) # => true
|
70
|
+
domain.owned? # => true
|
71
|
+
customer.owner?(domain) # => false (customers don't have owners in this example)
|
72
|
+
|
73
|
+
# Find owner
|
74
|
+
owner_objid = Customer.owners.get(domain.objid)
|
75
|
+
owner = Customer.find_by_objid(owner_objid)
|
76
|
+
```
|
77
|
+
|
78
|
+
## Advanced Features
|
79
|
+
|
80
|
+
### Custom Identifier Generation
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# Override ID generation for custom formats
|
84
|
+
class CustomModel < Familia::Horreum
|
85
|
+
feature :relatable_object
|
86
|
+
|
87
|
+
def self.generate_objid
|
88
|
+
# Custom UUID generation
|
89
|
+
"custom_#{SecureRandom.uuid_v7}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.generate_extid
|
93
|
+
# Custom external ID format
|
94
|
+
"ext_#{Time.now.to_i}_#{SecureRandom.hex(8)}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
### Ownership Management System
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
class OwnershipManager
|
103
|
+
# Grant ownership
|
104
|
+
def self.assign_owner(owned_object, owner_object)
|
105
|
+
# Validate objects are relatable
|
106
|
+
raise "Not relatable" unless owned_object.is_a?(V2::Features::RelatableObject)
|
107
|
+
raise "Not relatable" unless owner_object.is_a?(V2::Features::RelatableObject)
|
108
|
+
|
109
|
+
# Prevent self-ownership
|
110
|
+
raise "Self-ownership not allowed" if owned_object.class == owner_object.class
|
111
|
+
|
112
|
+
# Set ownership in registry
|
113
|
+
owner_object.class.owners.set(owned_object.objid, owner_object.objid)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Remove ownership
|
117
|
+
def self.remove_owner(owned_object)
|
118
|
+
owned_object.class.owners.delete(owned_object.objid)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Transfer ownership
|
122
|
+
def self.transfer_ownership(owned_object, new_owner)
|
123
|
+
remove_owner(owned_object)
|
124
|
+
assign_owner(owned_object, new_owner)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Get owner
|
128
|
+
def self.get_owner(owned_object, owner_class)
|
129
|
+
owner_objid = owned_object.class.owners.get(owned_object.objid)
|
130
|
+
return nil if owner_objid.nil? || owner_objid.empty?
|
131
|
+
|
132
|
+
owner_class.find_by_objid(owner_objid)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Usage
|
137
|
+
customer = Customer.new(name: "Acme Corp")
|
138
|
+
domain = Domain.new(name: "acme.com")
|
139
|
+
|
140
|
+
OwnershipManager.assign_owner(domain, customer)
|
141
|
+
OwnershipManager.get_owner(domain, Customer) # => customer object
|
142
|
+
```
|
143
|
+
|
144
|
+
### Multi-Tenant Patterns
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class Organization < Familia::Horreum
|
148
|
+
feature :relatable_object
|
149
|
+
|
150
|
+
field :name, :plan, :domain
|
151
|
+
|
152
|
+
def users
|
153
|
+
User.owned_by(self)
|
154
|
+
end
|
155
|
+
|
156
|
+
def projects
|
157
|
+
Project.owned_by(self)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class User < Familia::Horreum
|
162
|
+
feature :relatable_object
|
163
|
+
|
164
|
+
field :email, :name, :role
|
165
|
+
|
166
|
+
def self.owned_by(organization)
|
167
|
+
owned_objids = owners.keys.select do |user_objid|
|
168
|
+
owners.get(user_objid) == organization.objid
|
169
|
+
end
|
170
|
+
|
171
|
+
owned_objids.map { |objid| find_by_objid(objid) }.compact
|
172
|
+
end
|
173
|
+
|
174
|
+
def organization
|
175
|
+
org_objid = self.class.owners.get(objid)
|
176
|
+
Organization.find_by_objid(org_objid)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class Project < Familia::Horreum
|
181
|
+
feature :relatable_object
|
182
|
+
|
183
|
+
field :name, :description, :status
|
184
|
+
|
185
|
+
def self.owned_by(organization)
|
186
|
+
# Similar implementation as User.owned_by
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Usage
|
191
|
+
org = Organization.create(name: "Acme Corp")
|
192
|
+
user = User.create(email: "john@acme.com", name: "John Doe")
|
193
|
+
project = Project.create(name: "Website Redesign")
|
194
|
+
|
195
|
+
OwnershipManager.assign_owner(user, org)
|
196
|
+
OwnershipManager.assign_owner(project, org)
|
197
|
+
|
198
|
+
org.users # => [user]
|
199
|
+
org.projects # => [project]
|
200
|
+
user.organization # => org
|
201
|
+
```
|
202
|
+
|
203
|
+
### API Integration Patterns
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
class APIController
|
207
|
+
# Use external IDs in API responses
|
208
|
+
def show_customer
|
209
|
+
customer = Customer.find_by_objid(params[:objid]) # Internal lookup
|
210
|
+
|
211
|
+
render json: {
|
212
|
+
id: customer.extid, # External ID for API consumers
|
213
|
+
name: customer.name,
|
214
|
+
email: customer.email,
|
215
|
+
api_version: customer.api_version,
|
216
|
+
domains: customer_domains(customer)
|
217
|
+
}
|
218
|
+
end
|
219
|
+
|
220
|
+
# Accept external IDs in API requests
|
221
|
+
def update_customer
|
222
|
+
# Convert external ID to internal lookup
|
223
|
+
extid = params[:id]
|
224
|
+
objid = resolve_external_id(extid, Customer)
|
225
|
+
customer = Customer.find_by_objid(objid)
|
226
|
+
|
227
|
+
customer.update(customer_params)
|
228
|
+
|
229
|
+
render json: { id: customer.extid, status: 'updated' }
|
230
|
+
end
|
231
|
+
|
232
|
+
private
|
233
|
+
|
234
|
+
def customer_domains(customer)
|
235
|
+
Domain.owned_by(customer).map do |domain|
|
236
|
+
{
|
237
|
+
id: domain.extid,
|
238
|
+
name: domain.name,
|
239
|
+
dns_zone: domain.dns_zone
|
240
|
+
}
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def resolve_external_id(extid, klass)
|
245
|
+
# Implementation depends on your external ID tracking system
|
246
|
+
# This could be a separate mapping table or embedded in the extid format
|
247
|
+
ExternalIdMapping.objid_for(extid, klass)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
## Object Lifecycle Management
|
253
|
+
|
254
|
+
### Creation with Relationships
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
class CustomerCreationService
|
258
|
+
def self.create_with_domain(customer_attrs, domain_name)
|
259
|
+
customer = Customer.create(customer_attrs)
|
260
|
+
domain = Domain.create(name: domain_name)
|
261
|
+
|
262
|
+
# Establish ownership
|
263
|
+
OwnershipManager.assign_owner(domain, customer)
|
264
|
+
|
265
|
+
{
|
266
|
+
customer: customer,
|
267
|
+
domain: domain,
|
268
|
+
customer_id: customer.extid, # For API responses
|
269
|
+
domain_id: domain.extid
|
270
|
+
}
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Usage
|
275
|
+
result = CustomerCreationService.create_with_domain(
|
276
|
+
{ name: "Acme Corp", email: "admin@acme.com" },
|
277
|
+
"acme.com"
|
278
|
+
)
|
279
|
+
|
280
|
+
puts result[:customer_id] # => "ext_3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z"
|
281
|
+
```
|
282
|
+
|
283
|
+
### Deletion with Cleanup
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
class CustomerDeletionService
|
287
|
+
def self.delete_with_owned_objects(customer)
|
288
|
+
# Find all owned objects
|
289
|
+
owned_domains = Domain.owned_by(customer)
|
290
|
+
owned_users = User.owned_by(customer)
|
291
|
+
owned_projects = Project.owned_by(customer)
|
292
|
+
|
293
|
+
# Delete owned objects first
|
294
|
+
owned_domains.each(&:delete)
|
295
|
+
owned_users.each(&:delete)
|
296
|
+
owned_projects.each(&:delete)
|
297
|
+
|
298
|
+
# Clean up ownership records
|
299
|
+
[Domain, User, Project].each do |klass|
|
300
|
+
cleanup_ownership_records(klass, customer)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Finally delete the owner
|
304
|
+
customer.delete
|
305
|
+
end
|
306
|
+
|
307
|
+
private
|
308
|
+
|
309
|
+
def self.cleanup_ownership_records(owned_class, owner)
|
310
|
+
# Remove ownership records where this customer was the owner
|
311
|
+
owned_class.owners.keys.each do |owned_objid|
|
312
|
+
if owned_class.owners.get(owned_objid) == owner.objid
|
313
|
+
owned_class.owners.delete(owned_objid)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
## Security Considerations
|
321
|
+
|
322
|
+
### Access Control
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
class SecureAccessManager
|
326
|
+
def self.verify_ownership(user, resource)
|
327
|
+
return false unless user.is_a?(V2::Features::RelatableObject)
|
328
|
+
return false unless resource.is_a?(V2::Features::RelatableObject)
|
329
|
+
|
330
|
+
# Check direct ownership
|
331
|
+
return true if resource.owner?(user)
|
332
|
+
|
333
|
+
# Check organizational ownership
|
334
|
+
user_org = OwnershipManager.get_owner(user, Organization)
|
335
|
+
return true if user_org && resource.owner?(user_org)
|
336
|
+
|
337
|
+
false
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.filter_owned_resources(user, resources)
|
341
|
+
resources.select { |resource| verify_ownership(user, resource) }
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# In your controllers
|
346
|
+
class ResourceController
|
347
|
+
before_action :authenticate_user
|
348
|
+
before_action :load_resource, only: [:show, :update, :destroy]
|
349
|
+
before_action :verify_access, only: [:show, :update, :destroy]
|
350
|
+
|
351
|
+
private
|
352
|
+
|
353
|
+
def load_resource
|
354
|
+
@resource = Resource.find_by_objid(params[:objid])
|
355
|
+
not_found unless @resource
|
356
|
+
end
|
357
|
+
|
358
|
+
def verify_access
|
359
|
+
unless SecureAccessManager.verify_ownership(current_user, @resource)
|
360
|
+
unauthorized
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
```
|
365
|
+
|
366
|
+
### External ID Security
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
# Prevent external ID enumeration attacks
|
370
|
+
class SecureExternalIdGenerator
|
371
|
+
def self.generate_secure_extid(objid)
|
372
|
+
# Use HMAC to prevent guessing
|
373
|
+
timestamp = Time.now.to_i
|
374
|
+
data = "#{objid}:#{timestamp}"
|
375
|
+
hmac = OpenSSL::HMAC.hexdigest('SHA256', Rails.application.secret_key_base, data)
|
376
|
+
|
377
|
+
"ext_#{timestamp}_#{hmac[0..15]}" # 54 characters total
|
378
|
+
end
|
379
|
+
|
380
|
+
def self.validate_extid(extid, objid)
|
381
|
+
# Verify HMAC to prevent tampering
|
382
|
+
parts = extid.split('_')
|
383
|
+
return false unless parts.length == 3 && parts[0] == 'ext'
|
384
|
+
|
385
|
+
timestamp, provided_hmac = parts[1], parts[2]
|
386
|
+
expected_data = "#{objid}:#{timestamp}"
|
387
|
+
expected_hmac = OpenSSL::HMAC.hexdigest('SHA256', Rails.application.secret_key_base, expected_data)
|
388
|
+
|
389
|
+
expected_hmac[0..15] == provided_hmac
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Override in your models
|
394
|
+
class SecureCustomer < Familia::Horreum
|
395
|
+
feature :relatable_object
|
396
|
+
|
397
|
+
def self.generate_extid
|
398
|
+
# Will be called after objid is generated
|
399
|
+
SecureExternalIdGenerator.generate_secure_extid(objid)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
```
|
403
|
+
|
404
|
+
## Performance Optimization
|
405
|
+
|
406
|
+
### Batch Operations
|
407
|
+
|
408
|
+
```ruby
|
409
|
+
class BatchOwnershipManager
|
410
|
+
def self.assign_multiple_owners(ownership_pairs)
|
411
|
+
# ownership_pairs: [{ owned: obj1, owner: obj2 }, ...]
|
412
|
+
|
413
|
+
# Group by owner class to minimize pipe operations
|
414
|
+
grouped = ownership_pairs.group_by { |pair| pair[:owner].class }
|
415
|
+
|
416
|
+
grouped.each do |owner_class, pairs|
|
417
|
+
owner_class.owners.pipeline do |pipe|
|
418
|
+
pairs.each do |pair|
|
419
|
+
pipe.hset(pair[:owned].objid, pair[:owner].objid)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def self.load_owners_for_objects(objects, owner_class)
|
426
|
+
return {} if objects.empty?
|
427
|
+
|
428
|
+
# Batch load all ownership records
|
429
|
+
objids = objects.map(&:objid)
|
430
|
+
owner_objids = owner_class.owners.mget(*objids)
|
431
|
+
|
432
|
+
# Batch load owner objects
|
433
|
+
valid_owner_objids = owner_objids.compact.uniq
|
434
|
+
owners = valid_owner_objids.map { |objid| owner_class.find_by_objid(objid) }.compact
|
435
|
+
owners_by_objid = owners.index_by(&:objid)
|
436
|
+
|
437
|
+
# Create mapping
|
438
|
+
objects.zip(owner_objids).to_h do |obj, owner_objid|
|
439
|
+
[obj, owner_objid ? owners_by_objid[owner_objid] : nil]
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Usage
|
445
|
+
domains = Domain.all
|
446
|
+
domain_owners = BatchOwnershipManager.load_owners_for_objects(domains, Customer)
|
447
|
+
|
448
|
+
domains.each do |domain|
|
449
|
+
owner = domain_owners[domain]
|
450
|
+
puts "#{domain.name} owned by #{owner&.name || 'nobody'}"
|
451
|
+
end
|
452
|
+
```
|
453
|
+
|
454
|
+
### Caching Strategies
|
455
|
+
|
456
|
+
```ruby
|
457
|
+
class CachedOwnershipLookup
|
458
|
+
CACHE_TTL = 5.minutes
|
459
|
+
|
460
|
+
def self.get_owner(owned_object, owner_class)
|
461
|
+
cache_key = "owner:#{owned_object.objid}:#{owner_class.name}"
|
462
|
+
|
463
|
+
cached = Rails.cache.read(cache_key)
|
464
|
+
return cached if cached
|
465
|
+
|
466
|
+
owner = OwnershipManager.get_owner(owned_object, owner_class)
|
467
|
+
Rails.cache.write(cache_key, owner, expires_in: CACHE_TTL)
|
468
|
+
|
469
|
+
owner
|
470
|
+
end
|
471
|
+
|
472
|
+
def self.invalidate_ownership_cache(owned_object)
|
473
|
+
# Invalidate all possible owner class caches
|
474
|
+
[Customer, Organization, User].each do |owner_class|
|
475
|
+
cache_key = "owner:#{owned_object.objid}:#{owner_class.name}"
|
476
|
+
Rails.cache.delete(cache_key)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
```
|
481
|
+
|
482
|
+
## Testing
|
483
|
+
|
484
|
+
### RSpec Testing
|
485
|
+
|
486
|
+
```ruby
|
487
|
+
RSpec.describe RelatableObject do
|
488
|
+
let(:customer) { Customer.create(name: "Test Customer") }
|
489
|
+
let(:domain) { Domain.create(name: "test.com") }
|
490
|
+
|
491
|
+
describe "ID generation" do
|
492
|
+
it "generates UUID v7 for objid" do
|
493
|
+
expect(customer.objid).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i)
|
494
|
+
end
|
495
|
+
|
496
|
+
it "generates 54-character external ID" do
|
497
|
+
expect(customer.extid).to start_with('ext_')
|
498
|
+
expect(customer.extid.length).to eq(54)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
describe "ownership" do
|
503
|
+
before do
|
504
|
+
OwnershipManager.assign_owner(domain, customer)
|
505
|
+
end
|
506
|
+
|
507
|
+
it "establishes ownership relationship" do
|
508
|
+
expect(domain.owner?(customer)).to be true
|
509
|
+
expect(domain.owned?).to be true
|
510
|
+
end
|
511
|
+
|
512
|
+
it "prevents self-ownership" do
|
513
|
+
expect {
|
514
|
+
OwnershipManager.assign_owner(customer, customer)
|
515
|
+
}.to raise_error(/self-ownership/i)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
```
|
520
|
+
|
521
|
+
### Integration Testing
|
522
|
+
|
523
|
+
```ruby
|
524
|
+
RSpec.describe "RelatableObjects Integration" do
|
525
|
+
scenario "multi-tenant customer with domains and users" do
|
526
|
+
# Create organization
|
527
|
+
org = Organization.create(name: "Acme Corp")
|
528
|
+
|
529
|
+
# Create users and domains
|
530
|
+
user1 = User.create(email: "john@acme.com")
|
531
|
+
user2 = User.create(email: "jane@acme.com")
|
532
|
+
domain = Domain.create(name: "acme.com")
|
533
|
+
|
534
|
+
# Establish ownership
|
535
|
+
OwnershipManager.assign_owner(user1, org)
|
536
|
+
OwnershipManager.assign_owner(user2, org)
|
537
|
+
OwnershipManager.assign_owner(domain, org)
|
538
|
+
|
539
|
+
# Verify relationships
|
540
|
+
expect(org.users).to contain_exactly(user1, user2)
|
541
|
+
expect(org.domains).to contain_exactly(domain)
|
542
|
+
expect(user1.organization).to eq(org)
|
543
|
+
expect(domain.owner?(org)).to be true
|
544
|
+
|
545
|
+
# Test access control
|
546
|
+
expect(SecureAccessManager.verify_ownership(user1, domain)).to be true
|
547
|
+
expect(SecureAccessManager.verify_ownership(user2, domain)).to be true
|
548
|
+
end
|
549
|
+
end
|
550
|
+
```
|
551
|
+
|
552
|
+
## Best Practices
|
553
|
+
|
554
|
+
1. **Consistent ID Usage**: Always use external IDs in APIs, internal objids for system operations
|
555
|
+
2. **Ownership Validation**: Validate ownership before allowing operations
|
556
|
+
3. **Batch Operations**: Use batch loading for performance when dealing with multiple objects
|
557
|
+
4. **Cache Appropriately**: Cache ownership lookups but invalidate on changes
|
558
|
+
5. **Secure External IDs**: Use HMACs or similar to prevent ID enumeration attacks
|
559
|
+
6. **Clean Deletion**: Always clean up ownership records when deleting objects
|
560
|
+
7. **Type Safety**: Validate object types in ownership operations
|
561
|
+
8. **API Versioning**: Use the api_version field to handle API evolution
|
562
|
+
|
563
|
+
The RelatableObjects feature provides a robust foundation for building multi-tenant applications with secure object relationships and clear ownership models.
|