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
data/docs/wiki/Security-Model.md
CHANGED
@@ -2,19 +2,25 @@
|
|
2
2
|
|
3
3
|
## Cryptographic Design
|
4
4
|
|
5
|
+
### Provider-Based Architecture
|
6
|
+
|
7
|
+
Familia uses a modular provider system that automatically selects the best available encryption algorithm:
|
8
|
+
|
5
9
|
### Encryption Algorithms
|
6
10
|
|
7
|
-
**
|
8
|
-
-
|
9
|
-
- Key Size: 256 bits
|
10
|
-
- Nonce Size: 192 bits
|
11
|
-
- Authentication Tag: 128 bits
|
11
|
+
**XChaCha20-Poly1305 Provider (Priority: 100)**
|
12
|
+
- Requires: `rbnacl` gem (libsodium bindings)
|
13
|
+
- Key Size: 256 bits (32 bytes)
|
14
|
+
- Nonce Size: 192 bits (24 bytes) - extended nonce space
|
15
|
+
- Authentication Tag: 128 bits (16 bytes)
|
16
|
+
- Key Derivation: BLAKE2b with personalization string
|
12
17
|
|
13
|
-
**
|
14
|
-
-
|
15
|
-
- Key Size: 256 bits
|
16
|
-
-
|
17
|
-
- Authentication Tag: 128 bits
|
18
|
+
**AES-256-GCM Provider (Priority: 50)**
|
19
|
+
- Requires: OpenSSL (always available)
|
20
|
+
- Key Size: 256 bits (32 bytes)
|
21
|
+
- Nonce Size: 96 bits (12 bytes) - standard GCM nonce
|
22
|
+
- Authentication Tag: 128 bits (16 bytes)
|
23
|
+
- Key Derivation: HKDF-SHA256
|
18
24
|
|
19
25
|
### Key Derivation
|
20
26
|
|
@@ -26,19 +32,40 @@ Field Key = KDF(Master Key, Context)
|
|
26
32
|
Where Context = "ClassName:field_name:record_identifier"
|
27
33
|
```
|
28
34
|
|
29
|
-
**KDF
|
30
|
-
-
|
31
|
-
-
|
35
|
+
**Provider-Specific KDF:**
|
36
|
+
- **XChaCha20-Poly1305**: BLAKE2b with customizable personalization string
|
37
|
+
- **AES-256-GCM**: HKDF-SHA256 with salt and info parameters
|
38
|
+
|
39
|
+
The personalization string provides cryptographic domain separation:
|
40
|
+
```ruby
|
41
|
+
Familia.configure do |config|
|
42
|
+
config.encryption_personalization = 'MyApp-2024' # Default: 'Familia'
|
43
|
+
end
|
44
|
+
```
|
32
45
|
|
33
46
|
### Ciphertext Format
|
34
47
|
|
48
|
+
The encrypted data is stored as JSON with algorithm-specific fields:
|
49
|
+
|
50
|
+
**XChaCha20-Poly1305:**
|
35
51
|
```json
|
36
52
|
{
|
37
|
-
"library": "libsodium",
|
38
53
|
"algorithm": "xchacha20poly1305",
|
39
|
-
"nonce": "
|
40
|
-
"ciphertext": "
|
41
|
-
"
|
54
|
+
"nonce": "base64_24_byte_nonce",
|
55
|
+
"ciphertext": "base64_encrypted_data",
|
56
|
+
"auth_tag": "base64_16_byte_tag",
|
57
|
+
"key_version": "v1"
|
58
|
+
}
|
59
|
+
```
|
60
|
+
|
61
|
+
**AES-256-GCM:**
|
62
|
+
```json
|
63
|
+
{
|
64
|
+
"algorithm": "aes-256-gcm",
|
65
|
+
"nonce": "base64_12_byte_iv",
|
66
|
+
"ciphertext": "base64_encrypted_data",
|
67
|
+
"auth_tag": "base64_16_byte_tag",
|
68
|
+
"key_version": "v1"
|
42
69
|
}
|
43
70
|
```
|
44
71
|
|
@@ -97,15 +124,28 @@ vault.love_letter(passphrase_value: user_passphrase)
|
|
97
124
|
|
98
125
|
### Memory Safety
|
99
126
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
127
|
+
**⚠️ Critical Ruby Memory Limitations:**
|
128
|
+
|
129
|
+
Ruby provides **NO** memory safety guarantees for cryptographic secrets. This affects ALL providers:
|
130
|
+
|
131
|
+
- **No secure memory wiping**: Ruby cannot guarantee memory zeroing
|
132
|
+
- **GC copying**: Garbage collector may copy secrets before cleanup
|
133
|
+
- **String operations**: Every `.dup`, `+`, or interpolation creates uncontrolled copies
|
134
|
+
- **Memory dumps**: Secrets may persist in swap files or core dumps
|
135
|
+
- **Finalizer uncertainty**: `ObjectSpace.define_finalizer` timing is unpredictable
|
136
|
+
|
137
|
+
**Provider-Specific Mitigations:**
|
138
|
+
|
139
|
+
Both providers attempt best-effort memory clearing:
|
140
|
+
- Call `.clear` on sensitive strings after use
|
141
|
+
- Set variables to `nil` when done
|
142
|
+
- Use finalizers for cleanup (no guarantees)
|
104
143
|
|
105
|
-
**
|
106
|
-
-
|
107
|
-
-
|
108
|
-
-
|
144
|
+
**Recommendation**: For production systems with high-security requirements, consider:
|
145
|
+
- Hardware Security Modules (HSMs)
|
146
|
+
- External key management services
|
147
|
+
- Languages with manual memory management (C, Rust)
|
148
|
+
- Cryptographic appliances with secure enclaves
|
109
149
|
|
110
150
|
### RedactedString
|
111
151
|
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# Transient Fields Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Transient fields provide secure handling of sensitive runtime data that should never be persisted to Redis/Valkey. Unlike encrypted fields, transient fields exist only in memory and are automatically wrapped in `RedactedString` for security.
|
6
|
+
|
7
|
+
## When to Use Transient Fields
|
8
|
+
|
9
|
+
Use transient fields for:
|
10
|
+
- API keys and tokens that change frequently
|
11
|
+
- Temporary passwords or passphrases
|
12
|
+
- Session-specific secrets
|
13
|
+
- Any sensitive data that should never touch persistent storage
|
14
|
+
- Debug or development secrets that need secure handling
|
15
|
+
|
16
|
+
## Basic Usage
|
17
|
+
|
18
|
+
### Define Transient Fields
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
class ApiClient < Familia::Horreum
|
22
|
+
feature :transient_fields
|
23
|
+
|
24
|
+
field :endpoint # Regular persistent field
|
25
|
+
transient_field :token # Transient field (not persisted)
|
26
|
+
transient_field :secret, as: :api_secret # Custom accessor name
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
### Working with Transient Fields
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
client = ApiClient.new(
|
34
|
+
endpoint: 'https://api.example.com',
|
35
|
+
token: ENV['API_TOKEN'],
|
36
|
+
secret: ENV['API_SECRET']
|
37
|
+
)
|
38
|
+
|
39
|
+
# Regular field persists
|
40
|
+
client.save
|
41
|
+
client.endpoint # => "https://api.example.com"
|
42
|
+
|
43
|
+
# Transient fields are RedactedString instances
|
44
|
+
puts client.token # => "[REDACTED]"
|
45
|
+
|
46
|
+
# Access the actual value safely
|
47
|
+
client.token.expose do |token|
|
48
|
+
response = HTTP.post(client.endpoint,
|
49
|
+
headers: { 'Authorization' => "Bearer #{token}" }
|
50
|
+
)
|
51
|
+
# Token value is only available within this block
|
52
|
+
end
|
53
|
+
|
54
|
+
# Explicit cleanup when done
|
55
|
+
client.token.clear!
|
56
|
+
```
|
57
|
+
|
58
|
+
## RedactedString Security
|
59
|
+
|
60
|
+
### Automatic Wrapping
|
61
|
+
|
62
|
+
All transient field values are automatically wrapped in `RedactedString`:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
client = ApiClient.new(token: 'secret123')
|
66
|
+
client.token.class # => RedactedString
|
67
|
+
```
|
68
|
+
|
69
|
+
### Safe Access Pattern
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# ✅ Recommended: Use .expose block
|
73
|
+
client.token.expose do |token|
|
74
|
+
# Use token directly without creating copies
|
75
|
+
HTTP.auth("Bearer #{token}") # Safe
|
76
|
+
end
|
77
|
+
|
78
|
+
# ✅ Direct access (use carefully)
|
79
|
+
raw_token = client.token.value
|
80
|
+
# Remember to clear original source if needed
|
81
|
+
|
82
|
+
# ❌ Avoid: These create uncontrolled copies
|
83
|
+
token_copy = client.token.value.dup # Creates copy in memory
|
84
|
+
interpolated = "Bearer #{client.token}" # Creates copy via to_s
|
85
|
+
```
|
86
|
+
|
87
|
+
### Memory Management
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# Clear individual fields
|
91
|
+
client.token.clear!
|
92
|
+
|
93
|
+
# Check if cleared
|
94
|
+
client.token.cleared? # => true
|
95
|
+
|
96
|
+
# Accessing cleared values raises error
|
97
|
+
client.token.value # => SecurityError: Value already cleared
|
98
|
+
```
|
99
|
+
|
100
|
+
## Advanced Features
|
101
|
+
|
102
|
+
### Custom Accessor Names
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class Service < Familia::Horreum
|
106
|
+
transient_field :api_key, as: :secret_key
|
107
|
+
end
|
108
|
+
|
109
|
+
service = Service.new(api_key: 'secret123')
|
110
|
+
service.secret_key.expose { |key| use_api_key(key) }
|
111
|
+
```
|
112
|
+
|
113
|
+
### Integration with Encrypted Fields
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class SecureService < Familia::Horreum
|
117
|
+
feature :transient_fields
|
118
|
+
|
119
|
+
encrypted_field :long_term_secret # Persisted, encrypted
|
120
|
+
transient_field :session_token # Runtime only, not persisted
|
121
|
+
field :public_endpoint # Normal field
|
122
|
+
end
|
123
|
+
|
124
|
+
service = SecureService.new(
|
125
|
+
long_term_secret: 'stored encrypted in Redis',
|
126
|
+
session_token: 'temporary runtime secret',
|
127
|
+
public_endpoint: 'https://api.example.com'
|
128
|
+
)
|
129
|
+
|
130
|
+
service.save
|
131
|
+
# Only long_term_secret and public_endpoint are saved to Redis
|
132
|
+
# session_token exists only in memory
|
133
|
+
```
|
134
|
+
|
135
|
+
## RedactedString API Reference
|
136
|
+
|
137
|
+
### Core Methods
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# Create (usually automatic via transient_field)
|
141
|
+
secret = RedactedString.new('sensitive_value')
|
142
|
+
|
143
|
+
# Safe access
|
144
|
+
secret.expose { |value| use_value(value) }
|
145
|
+
|
146
|
+
# Direct access (use with caution)
|
147
|
+
value = secret.value
|
148
|
+
|
149
|
+
# Cleanup
|
150
|
+
secret.clear!
|
151
|
+
|
152
|
+
# Status
|
153
|
+
secret.cleared? # => true/false
|
154
|
+
```
|
155
|
+
|
156
|
+
### Security Methods
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
# Logging/debugging protection
|
160
|
+
puts secret.to_s # => "[REDACTED]"
|
161
|
+
puts secret.inspect # => "[REDACTED]"
|
162
|
+
|
163
|
+
# Equality (object identity only)
|
164
|
+
secret1 == secret2 # => false (unless same object)
|
165
|
+
|
166
|
+
# Hash (constant for all instances)
|
167
|
+
secret.hash # => Same for all RedactedString instances
|
168
|
+
```
|
169
|
+
|
170
|
+
## Security Considerations
|
171
|
+
|
172
|
+
### Ruby Memory Limitations
|
173
|
+
|
174
|
+
**⚠️ Important**: Ruby provides no memory safety guarantees:
|
175
|
+
|
176
|
+
- **No secure wiping**: `.clear!` is best-effort only
|
177
|
+
- **GC copying**: Garbage collector may duplicate secrets
|
178
|
+
- **String operations**: Every manipulation creates copies
|
179
|
+
- **Memory persistence**: Secrets may remain in memory indefinitely
|
180
|
+
|
181
|
+
### Best Practices
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# ✅ Wrap immediately after input
|
185
|
+
password = RedactedString.new(params[:password])
|
186
|
+
params[:password] = nil # Clear original reference
|
187
|
+
|
188
|
+
# ✅ Use .expose for short operations
|
189
|
+
token.expose { |t| api_call(t) }
|
190
|
+
|
191
|
+
# ✅ Clear explicitly when done
|
192
|
+
token.clear!
|
193
|
+
|
194
|
+
# ✅ Avoid string operations that create copies
|
195
|
+
token.expose { |t| "Bearer #{t}" } # Creates copy
|
196
|
+
# Better: Pass token directly to methods that need it
|
197
|
+
|
198
|
+
# ❌ Don't pass RedactedString to logging
|
199
|
+
logger.info "Token: #{token}" # Still logs "[REDACTED]" but safer to avoid
|
200
|
+
|
201
|
+
# ❌ Don't store in instance variables outside field system
|
202
|
+
@raw_token = token.value # Creates uncontrolled copy
|
203
|
+
```
|
204
|
+
|
205
|
+
### Production Recommendations
|
206
|
+
|
207
|
+
For highly sensitive applications, consider:
|
208
|
+
- External secrets management (HashiCorp Vault, AWS Secrets Manager)
|
209
|
+
- Hardware Security Modules (HSMs)
|
210
|
+
- Languages with secure memory handling
|
211
|
+
- Encrypted swap and memory protection at OS level
|
212
|
+
|
213
|
+
## Integration Examples
|
214
|
+
|
215
|
+
### Rails Controller
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
class ApiController < ApplicationController
|
219
|
+
def authenticate
|
220
|
+
service = ApiService.new(
|
221
|
+
endpoint: params[:endpoint],
|
222
|
+
token: params[:token] # Auto-wrapped in RedactedString
|
223
|
+
)
|
224
|
+
|
225
|
+
result = service.token.expose do |token|
|
226
|
+
# Token only accessible within this block
|
227
|
+
ExternalAPI.authenticate(token)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Clear token when request is done
|
231
|
+
service.token.clear!
|
232
|
+
|
233
|
+
render json: { status: result }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
### Background Job
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
class ApiSyncJob
|
242
|
+
def perform(user_id, token)
|
243
|
+
user = User.find(user_id)
|
244
|
+
|
245
|
+
# Wrap external token securely
|
246
|
+
secure_token = RedactedString.new(token)
|
247
|
+
token.clear if token.respond_to?(:clear) # Clear original
|
248
|
+
|
249
|
+
client = ApiClient.new(token: secure_token)
|
250
|
+
|
251
|
+
begin
|
252
|
+
sync_data(client)
|
253
|
+
ensure
|
254
|
+
client.token.clear! # Always cleanup
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def sync_data(client)
|
261
|
+
client.token.expose do |token|
|
262
|
+
# Use token for API calls
|
263
|
+
fetch_and_process_data(token)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
## Comparison with Encrypted Fields
|
270
|
+
|
271
|
+
| Feature | Encrypted Fields | Transient Fields |
|
272
|
+
|---------|------------------|------------------|
|
273
|
+
| **Persistence** | Saved to Redis/Valkey | Memory only |
|
274
|
+
| **Encryption** | AES/XChaCha20 | None (not stored) |
|
275
|
+
| **Use Case** | Long-term secrets | Runtime secrets |
|
276
|
+
| **Access** | Automatic decrypt | RedactedString wrapper |
|
277
|
+
| **Performance** | Crypto overhead | No crypto operations |
|
278
|
+
| **Lifecycle** | Survives restarts | Cleared on restart |
|
279
|
+
|
280
|
+
Choose encrypted fields for data that must persist across sessions. Choose transient fields for sensitive runtime data that should never be stored.
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# examples/bit_encoding_integration.rb
|
2
|
+
#
|
3
|
+
# Production Integration Example: Document Management System with Fine-Grained Permissions
|
4
|
+
#
|
5
|
+
# This example demonstrates how to use Familia's bit encoding permission system
|
6
|
+
# in a real-world document management scenario with sophisticated access control.
|
7
|
+
|
8
|
+
require_relative '../lib/familia'
|
9
|
+
require_relative '../lib/familia/features/relationships/score_encoding'
|
10
|
+
require_relative '../lib/familia/features/relationships/permission_management'
|
11
|
+
|
12
|
+
# Document Management System Classes
|
13
|
+
class User < Familia::Horreum
|
14
|
+
logical_database 14
|
15
|
+
|
16
|
+
identifier_field :user_id
|
17
|
+
field :user_id
|
18
|
+
field :email
|
19
|
+
field :name
|
20
|
+
field :role # admin, editor, viewer, guest
|
21
|
+
field :created_at
|
22
|
+
|
23
|
+
sorted_set :documents # Documents this user can access
|
24
|
+
sorted_set :recent_activity # Recent document access
|
25
|
+
end
|
26
|
+
|
27
|
+
class Document < Familia::Horreum
|
28
|
+
include Familia::Features::Relationships::PermissionManagement
|
29
|
+
|
30
|
+
logical_database 14
|
31
|
+
|
32
|
+
# Enable fine-grained permission tracking
|
33
|
+
permission_tracking :user_permissions
|
34
|
+
|
35
|
+
identifier_field :doc_id
|
36
|
+
field :doc_id
|
37
|
+
field :title
|
38
|
+
field :owner_id
|
39
|
+
field :content
|
40
|
+
field :created_at
|
41
|
+
field :updated_at
|
42
|
+
field :document_type # public, private, confidential
|
43
|
+
|
44
|
+
sorted_set :collaborators # Users with access to this document
|
45
|
+
list :audit_log # Track permission changes and access
|
46
|
+
|
47
|
+
# Add document to user's collection with specific permissions
|
48
|
+
def share_with_user(user, *permissions)
|
49
|
+
permissions = [:read] if permissions.empty?
|
50
|
+
|
51
|
+
# Create time-based score with permissions encoded
|
52
|
+
timestamp = updated_at || Time.now
|
53
|
+
score = Familia::Features::Relationships::ScoreEncoding.encode_score(timestamp, permissions)
|
54
|
+
|
55
|
+
# Add to user's document list
|
56
|
+
user.documents.add(score, doc_id)
|
57
|
+
|
58
|
+
# Add user to document's collaborator list
|
59
|
+
collaborators.add(score, user.user_id)
|
60
|
+
|
61
|
+
# Grant permissions via permission management
|
62
|
+
grant(user, *permissions)
|
63
|
+
|
64
|
+
# Log the permission grant
|
65
|
+
log_entry = "#{Time.now.iso8601}: Granted #{permissions.join(', ')} to #{user.email}"
|
66
|
+
audit_log.push(log_entry)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Remove user access
|
70
|
+
def revoke_access(user)
|
71
|
+
user.documents.zrem(doc_id)
|
72
|
+
collaborators.zrem(user.user_id)
|
73
|
+
revoke(user, :read, :write, :edit, :delete, :configure, :transfer, :admin)
|
74
|
+
|
75
|
+
log_entry = "#{Time.now.iso8601}: Revoked all access from #{user.email}"
|
76
|
+
audit_log.push(log_entry)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get users with specific permission level or higher
|
80
|
+
def users_with_permission(*required_permissions)
|
81
|
+
all_permissions.select do |user_id, user_perms|
|
82
|
+
required_permissions.all? { |perm| user_perms.include?(perm) }
|
83
|
+
end.keys
|
84
|
+
end
|
85
|
+
|
86
|
+
# Advanced: Get document access history for analytics
|
87
|
+
def access_analytics(days_back = 30)
|
88
|
+
start_time = Time.now - (days_back * 24 * 60 * 60)
|
89
|
+
end_time = Time.now
|
90
|
+
|
91
|
+
# Use score range to get recent access
|
92
|
+
range = Familia::Features::Relationships::ScoreEncoding.score_range(
|
93
|
+
start_time,
|
94
|
+
end_time,
|
95
|
+
min_permissions: [:read]
|
96
|
+
)
|
97
|
+
|
98
|
+
# Get collaborators active in time range
|
99
|
+
active_users = collaborators.rangebyscore(*range)
|
100
|
+
|
101
|
+
{
|
102
|
+
active_users: active_users,
|
103
|
+
total_collaborators: collaborators.size,
|
104
|
+
permission_breakdown: all_permissions,
|
105
|
+
audit_entries: audit_log.range(0, 50)
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Document Management Service - Business Logic Layer
|
111
|
+
class DocumentService
|
112
|
+
# Permission role definitions matching business needs
|
113
|
+
ROLE_PERMISSIONS = {
|
114
|
+
guest: [:read],
|
115
|
+
viewer: [:read],
|
116
|
+
commenter: [:read, :append],
|
117
|
+
editor: [:read, :write, :edit],
|
118
|
+
reviewer: [:read, :write, :edit, :delete],
|
119
|
+
admin: [:read, :write, :edit, :delete, :configure, :transfer, :admin]
|
120
|
+
}.freeze
|
121
|
+
|
122
|
+
def self.create_document(owner, title, content, doc_type = 'private')
|
123
|
+
doc = Document.new(
|
124
|
+
doc_id: "doc_#{Time.now.to_i}_#{rand(1000)}",
|
125
|
+
title: title,
|
126
|
+
content: content,
|
127
|
+
owner_id: owner.user_id,
|
128
|
+
document_type: doc_type,
|
129
|
+
created_at: Time.now,
|
130
|
+
updated_at: Time.now
|
131
|
+
)
|
132
|
+
|
133
|
+
# Owner gets full admin access
|
134
|
+
doc.share_with_user(owner, *ROLE_PERMISSIONS[:admin])
|
135
|
+
doc
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.share_document(document, user, role)
|
139
|
+
permissions = ROLE_PERMISSIONS[role] || ROLE_PERMISSIONS[:viewer]
|
140
|
+
document.share_with_user(user, *permissions)
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.can_user_perform?(user, document, action)
|
144
|
+
case action
|
145
|
+
when :view, :read
|
146
|
+
document.can?(user, :read)
|
147
|
+
when :comment, :append
|
148
|
+
document.can?(user, :read, :append)
|
149
|
+
when :edit, :modify
|
150
|
+
document.can?(user, :read, :write, :edit)
|
151
|
+
when :delete, :remove
|
152
|
+
document.can?(user, :delete)
|
153
|
+
when :share, :configure
|
154
|
+
document.can?(user, :configure)
|
155
|
+
when :transfer_ownership
|
156
|
+
document.can?(user, :admin)
|
157
|
+
else
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.bulk_permission_update(documents, users, role)
|
163
|
+
permissions = ROLE_PERMISSIONS[role]
|
164
|
+
|
165
|
+
documents.each do |doc|
|
166
|
+
users.each do |user|
|
167
|
+
doc.revoke_access(user) # Clear existing
|
168
|
+
doc.share_with_user(user, *permissions) if permissions
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Example Usage and Demonstration
|
175
|
+
if __FILE__ == $0
|
176
|
+
puts "🚀 Familia Bit Encoding Integration Example"
|
177
|
+
puts "=" * 50
|
178
|
+
|
179
|
+
# Create users
|
180
|
+
alice = User.new(user_id: 'alice', email: 'alice@company.com', name: 'Alice Smith', role: 'admin')
|
181
|
+
bob = User.new(user_id: 'bob', email: 'bob@company.com', name: 'Bob Jones', role: 'editor')
|
182
|
+
charlie = User.new(user_id: 'charlie', email: 'charlie@company.com', name: 'Charlie Brown', role: 'viewer')
|
183
|
+
|
184
|
+
# Create documents
|
185
|
+
doc1 = DocumentService.create_document(alice, "Q4 Financial Report", "Confidential financial data...", 'confidential')
|
186
|
+
doc2 = DocumentService.create_document(alice, "Team Meeting Notes", "Weekly standup notes...", 'private')
|
187
|
+
doc3 = DocumentService.create_document(bob, "Project Proposal", "New feature proposal...", 'public')
|
188
|
+
|
189
|
+
# Share documents with different permission levels
|
190
|
+
puts "\n📄 Document Sharing:"
|
191
|
+
DocumentService.share_document(doc1, bob, :reviewer) # Bob can review financial report
|
192
|
+
DocumentService.share_document(doc1, charlie, :viewer) # Charlie can only view
|
193
|
+
|
194
|
+
DocumentService.share_document(doc2, bob, :editor) # Bob can edit meeting notes
|
195
|
+
DocumentService.share_document(doc2, charlie, :commenter) # Charlie can comment
|
196
|
+
|
197
|
+
DocumentService.share_document(doc3, alice, :admin) # Alice gets admin on Bob's doc
|
198
|
+
DocumentService.share_document(doc3, charlie, :editor) # Charlie can edit proposal
|
199
|
+
|
200
|
+
# Test permission checks
|
201
|
+
puts "\n🔐 Permission Testing:"
|
202
|
+
puts "Can Bob edit financial report? #{DocumentService.can_user_perform?(bob, doc1, :edit)}"
|
203
|
+
puts "Can Bob delete financial report? #{DocumentService.can_user_perform?(bob, doc1, :delete)}"
|
204
|
+
puts "Can Charlie comment on meeting notes? #{DocumentService.can_user_perform?(charlie, doc2, :comment)}"
|
205
|
+
puts "Can Charlie edit project proposal? #{DocumentService.can_user_perform?(charlie, doc3, :edit)}"
|
206
|
+
|
207
|
+
# Advanced analytics
|
208
|
+
puts "\n📊 Document Analytics:"
|
209
|
+
analytics = doc1.access_analytics
|
210
|
+
puts "Financial Report - Active Users: #{analytics[:active_users].size}"
|
211
|
+
puts "Total Collaborators: #{analytics[:total_collaborators]}"
|
212
|
+
puts "Permission Breakdown:"
|
213
|
+
analytics[:permission_breakdown].each do |user_id, perms|
|
214
|
+
puts " #{user_id}: #{perms.join(', ')}"
|
215
|
+
end
|
216
|
+
|
217
|
+
# Demonstrate bit encoding efficiency
|
218
|
+
puts "\n⚡ Bit Encoding Efficiency:"
|
219
|
+
score = Familia::Features::Relationships::ScoreEncoding.encode_score(Time.now, [:read, :write, :edit, :delete])
|
220
|
+
decoded = Familia::Features::Relationships::ScoreEncoding.decode_score(score)
|
221
|
+
puts "Encoded score: #{score}"
|
222
|
+
puts "Decoded permissions: #{decoded[:permission_list].join(', ')}"
|
223
|
+
puts "Permission bits: #{decoded[:permissions]} (#{decoded[:permissions].to_s(2).rjust(8, '0')})"
|
224
|
+
|
225
|
+
# Cleanup
|
226
|
+
puts "\n🧹 Cleanup:"
|
227
|
+
[alice, bob, charlie].each { |user| user.documents.clear }
|
228
|
+
[doc1, doc2, doc3].each { |doc| doc.clear_all_permissions; doc.collaborators.clear }
|
229
|
+
|
230
|
+
puts "✅ Integration example completed successfully!"
|
231
|
+
puts "\nThis demonstrates:"
|
232
|
+
puts "• Fine-grained permission management with 8-bit encoding"
|
233
|
+
puts "• Role-based access control with business logic"
|
234
|
+
puts "• Time-based analytics and audit trails"
|
235
|
+
puts "• Efficient Redis storage with sorted sets"
|
236
|
+
puts "• Production-ready error handling and validation"
|
237
|
+
end
|