asherah 0.9.1-aarch64-linux → 0.10.1-aarch64-linux
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/README.md +336 -56
- data/lib/asherah/config.rb +64 -74
- data/lib/asherah/error.rb +11 -25
- data/lib/asherah/hooks.rb +239 -0
- data/lib/asherah/native/{libasherah-arm64.so → libasherah_ffi.so} +0 -0
- data/lib/asherah/native.rb +146 -0
- data/lib/asherah/session.rb +176 -0
- data/lib/asherah/session_factory.rb +35 -0
- data/lib/asherah/version.rb +1 -1
- data/lib/asherah.rb +286 -100
- metadata +43 -39
- data/.env.secrets.example +0 -9
- data/.rspec +0 -3
- data/.rubocop.yml +0 -112
- data/.ruby-version +0 -1
- data/CHANGELOG.md +0 -135
- data/CODE_OF_CONDUCT.md +0 -77
- data/CONTRIBUTING.md +0 -118
- data/Gemfile +0 -14
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -29
- data/SECURITY.md +0 -19
- data/asherah.gemspec +0 -39
- data/ext/asherah/checksums.yml +0 -5
- data/ext/asherah/extconf.rb +0 -7
- data/ext/asherah/native_file.rb +0 -64
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7c5fccb13d0c1617f5b063ff214e76bdd1f8003d5858baae801ea7e52f663d5
|
|
4
|
+
data.tar.gz: 1332860879b52ff9c58dbd14515bc5ae361d993306e545ff1ae84dd4d207f5bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a2941c03f970770ad1218095bd1b7082b749b8bd21a2732e2974abef1be402d4f251f29716dc9eb67bd5ae7a79d67b1adeb86d8d1efe04ca49e00b3de53ead9
|
|
7
|
+
data.tar.gz: 8e4dee620411e8d5b868f3902b746805e06b245c2f35f10bf8b26bbafdaf1e463bcab999c0e08fe55cdc86af7f897190d19e6a21ae359d0e54d9de8a11e3eb13
|
data/README.md
CHANGED
|
@@ -1,106 +1,386 @@
|
|
|
1
|
-
#
|
|
1
|
+
# asherah
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ruby bindings for [Asherah](https://github.com/godaddy/asherah-ffi) envelope encryption with automatic key rotation.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- [Design and Architecture](https://github.com/godaddy/asherah/blob/master/docs/DesignAndArchitecture.md)
|
|
8
|
-
- [Key Caching](https://github.com/godaddy/asherah/blob/master/docs/KeyCaching.md)
|
|
9
|
-
- [Key Management Service](https://github.com/godaddy/asherah/blob/master/docs/KeyManagementService.md)
|
|
10
|
-
- [Metastore](https://github.com/godaddy/asherah/blob/master/docs/Metastore.md)
|
|
11
|
-
- [System Requirements](https://github.com/godaddy/asherah/blob/master/docs/SystemRequirements.md)
|
|
12
|
-
|
|
13
|
-
## Supported Platforms
|
|
14
|
-
|
|
15
|
-
Currently supported platforms are Linux and Darwin operating systems for x64 and arm64 CPU architectures.
|
|
5
|
+
Published to [RubyGems](https://rubygems.org/gems/asherah) with prebuilt native libraries for Linux x64/ARM64 (glibc and musl/Alpine) and macOS x64/ARM64. A fallback source gem is available for other platforms (requires the Rust toolchain to compile).
|
|
16
6
|
|
|
17
7
|
## Installation
|
|
18
8
|
|
|
19
|
-
|
|
9
|
+
```bash
|
|
10
|
+
gem install asherah
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add to your Gemfile:
|
|
20
14
|
|
|
21
15
|
```ruby
|
|
22
16
|
gem 'asherah'
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
bundle install
|
|
27
|
-
```
|
|
19
|
+
The gem uses FFI to load the native Asherah library. Platform-specific gems ship the prebuilt library; the source gem builds it during installation.
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
## Documentation
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
Task-oriented walkthroughs under [`docs/`](./docs/):
|
|
24
|
+
|
|
25
|
+
| Guide | When to read |
|
|
26
|
+
|---|---|
|
|
27
|
+
| [Getting started](./docs/getting-started.md) | `gem install` through round-trip encrypt/decrypt. |
|
|
28
|
+
| [Framework integration](./docs/framework-integration.md) | Rails, Sidekiq, Sinatra, Rack middleware, AWS Lambda. |
|
|
29
|
+
| [AWS production setup](./docs/aws-production-setup.md) | KMS keys, DynamoDB, IAM policy, region routing. |
|
|
30
|
+
| [Testing](./docs/testing.md) | RSpec/Minitest fixtures, Testcontainers, mocking patterns. |
|
|
31
|
+
| [Troubleshooting](./docs/troubleshooting.md) | Common errors with what to check first. |
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
The simplest way to use Asherah is the static module API. Call `setup` once at startup and `shutdown` on exit:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "asherah"
|
|
39
|
+
|
|
40
|
+
Asherah.setup(
|
|
41
|
+
"ServiceName" => "my-service",
|
|
42
|
+
"ProductID" => "my-product",
|
|
43
|
+
"Metastore" => "memory", # testing only
|
|
44
|
+
"KMS" => "static" # testing only
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
ciphertext = Asherah.encrypt_string("partition-id", "sensitive data")
|
|
48
|
+
plaintext = Asherah.decrypt_string("partition-id", ciphertext)
|
|
49
|
+
|
|
50
|
+
Asherah.shutdown
|
|
33
51
|
```
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
The static API manages a session cache internally. Sessions are created on first use per partition and reused for subsequent calls.
|
|
54
|
+
|
|
55
|
+
### Block-style configuration
|
|
36
56
|
|
|
37
|
-
|
|
57
|
+
For an API compatible with the canonical GoDaddy Asherah Ruby gem, use `configure` with a block:
|
|
38
58
|
|
|
39
59
|
```ruby
|
|
40
60
|
Asherah.configure do |config|
|
|
41
|
-
config.
|
|
42
|
-
config.
|
|
43
|
-
config.
|
|
44
|
-
config.
|
|
61
|
+
config.service_name = "my-service"
|
|
62
|
+
config.product_id = "my-product"
|
|
63
|
+
config.kms = "static" # testing only
|
|
64
|
+
config.metastore = "memory" # testing only
|
|
45
65
|
end
|
|
66
|
+
|
|
67
|
+
ciphertext = Asherah.encrypt_string("partition-id", "sensitive data")
|
|
68
|
+
plaintext = Asherah.decrypt_string("partition-id", ciphertext)
|
|
69
|
+
|
|
70
|
+
Asherah.shutdown
|
|
46
71
|
```
|
|
47
72
|
|
|
48
|
-
|
|
73
|
+
## Session-Based API
|
|
49
74
|
|
|
50
|
-
|
|
75
|
+
For direct control over session lifecycle, use `SessionFactory` and `Session`:
|
|
51
76
|
|
|
52
77
|
```ruby
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
require "asherah"
|
|
79
|
+
|
|
80
|
+
Asherah.configure do |config|
|
|
81
|
+
config.service_name = "my-service"
|
|
82
|
+
config.product_id = "my-product"
|
|
83
|
+
config.kms = "static" # testing only
|
|
84
|
+
config.metastore = "memory" # testing only
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
factory = Asherah::SessionFactory.new(
|
|
88
|
+
Asherah::Native.asherah_factory_new_with_config(config_json)
|
|
89
|
+
)
|
|
90
|
+
session = factory.get_session("partition-id")
|
|
91
|
+
|
|
92
|
+
ciphertext = session.encrypt_bytes("sensitive data")
|
|
93
|
+
plaintext = session.decrypt_bytes(ciphertext)
|
|
94
|
+
|
|
95
|
+
session.close
|
|
96
|
+
factory.close
|
|
57
97
|
```
|
|
58
98
|
|
|
59
|
-
|
|
99
|
+
Or via the static API's internal factory (the typical pattern):
|
|
60
100
|
|
|
61
101
|
```ruby
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
102
|
+
Asherah.setup("ServiceName" => "my-service", "ProductID" => "my-product",
|
|
103
|
+
"Metastore" => "memory", "KMS" => "static") # testing only
|
|
65
104
|
|
|
66
|
-
|
|
105
|
+
# The static API acquires and caches sessions automatically
|
|
106
|
+
ct = Asherah.encrypt("partition-id", "data")
|
|
107
|
+
pt = Asherah.decrypt("partition-id", ct)
|
|
67
108
|
|
|
68
|
-
|
|
109
|
+
Asherah.shutdown
|
|
110
|
+
```
|
|
69
111
|
|
|
70
|
-
|
|
112
|
+
## Async API
|
|
71
113
|
|
|
72
|
-
###
|
|
114
|
+
### Session-level async (true async via Rust tokio)
|
|
73
115
|
|
|
74
|
-
|
|
116
|
+
The session's async methods dispatch work to Rust's tokio runtime and receive results via FFI callbacks:
|
|
75
117
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- Go 1.24+ installed
|
|
118
|
+
```ruby
|
|
119
|
+
session = factory.get_session("partition-id")
|
|
79
120
|
|
|
80
|
-
|
|
121
|
+
ct = session.encrypt_bytes_async(data)
|
|
122
|
+
pt = session.decrypt_bytes_async(ct)
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
TEST_DB_PASSWORD=pass bin/cross-language-test.sh
|
|
124
|
+
session.close
|
|
84
125
|
```
|
|
85
126
|
|
|
86
|
-
|
|
127
|
+
### Static-level async (thread-based)
|
|
128
|
+
|
|
129
|
+
The static API's async methods run in a Ruby `Thread`:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
thread = Asherah.encrypt_async("partition-id", data) do |result|
|
|
133
|
+
puts "Encrypted: #{result.bytesize} bytes"
|
|
134
|
+
end
|
|
135
|
+
thread.join
|
|
136
|
+
```
|
|
87
137
|
|
|
88
|
-
|
|
138
|
+
### Async Behavior
|
|
139
|
+
|
|
140
|
+
The session-level async methods (`encrypt_bytes_async`, `decrypt_bytes_async`) are true async. The encrypt/decrypt work runs on Rust's tokio worker threads and completes via an FFI callback. The Ruby interpreter is NOT blocked during the native call.
|
|
141
|
+
|
|
142
|
+
However, the implementation uses `Queue#pop` to synchronize the callback result back to the calling Ruby thread. This means `queue.pop` blocks the calling Ruby thread until the result arrives. True concurrency requires multiple Ruby threads or Ractors dispatching async calls in parallel.
|
|
143
|
+
|
|
144
|
+
If the FFI callback never fires (e.g. the worker pool deadlocks), the
|
|
145
|
+
async call raises `Asherah::Error::Timeout` after 30 seconds rather
|
|
146
|
+
than blocking indefinitely. Override the bound by setting the
|
|
147
|
+
`ASHERAH_RUBY_ASYNC_TIMEOUT` environment variable (in seconds) before
|
|
148
|
+
the gem is loaded.
|
|
149
|
+
|
|
150
|
+
The static-level async methods (`Asherah.encrypt_async`, `Asherah.decrypt_async`) simply run the sync operation in a new `Thread`.
|
|
151
|
+
|
|
152
|
+
## Input contract
|
|
153
|
+
|
|
154
|
+
**Partition ID** (`nil`, `""`): always rejected as programming errors
|
|
155
|
+
with `ArgumentError` (`"partition_id cannot be empty"`). No row is ever
|
|
156
|
+
written to the metastore under a degenerate partition ID.
|
|
157
|
+
|
|
158
|
+
**Plaintext** to encrypt:
|
|
159
|
+
- `nil` → `ArgumentError` from explicit guards in the public API before
|
|
160
|
+
any FFI call.
|
|
161
|
+
- Empty `String` (`""`) and empty bytes (`"".b`) are **valid**
|
|
162
|
+
plaintexts. `Asherah.encrypt_string` / `session.encrypt_bytes`
|
|
163
|
+
produce a real `DataRowRecord` envelope; the matching decrypt returns
|
|
164
|
+
exactly `""` or empty bytes.
|
|
165
|
+
|
|
166
|
+
**Ciphertext** to decrypt:
|
|
167
|
+
- `nil` → `ArgumentError`.
|
|
168
|
+
- Empty `String` → `Asherah::Error::DecryptFailed` (not valid
|
|
169
|
+
`DataRowRecord` JSON).
|
|
170
|
+
|
|
171
|
+
**Do not short-circuit empty plaintext encryption in caller code** —
|
|
172
|
+
empty data is real data, encrypting it produces a genuine envelope, and
|
|
173
|
+
skipping encryption leaks the fact that the value was empty. See
|
|
174
|
+
[docs/input-contract.md](../docs/input-contract.md) for the full
|
|
175
|
+
rationale.
|
|
176
|
+
|
|
177
|
+
## Migration from Canonical Ruby SDK
|
|
178
|
+
|
|
179
|
+
This replaces the original `asherah` gem which was built on Go via Cobhan FFI. The API is drop-in compatible:
|
|
180
|
+
|
|
181
|
+
| | Canonical (Go/Cobhan) | This binding (Rust/FFI) |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| Implementation | Go + Cobhan FFI | Rust + Ruby FFI gem |
|
|
184
|
+
| `Asherah.configure` | Supported | Supported (same API) |
|
|
185
|
+
| `Asherah.encrypt` / `decrypt` | Supported | Supported (same API) |
|
|
186
|
+
| `SessionFactory` | Supported | Supported (same API) |
|
|
187
|
+
| Memory protection | None | memguard (locked, wiped pages) |
|
|
188
|
+
| Async support | None | Session-level true async |
|
|
189
|
+
|
|
190
|
+
Migration steps:
|
|
191
|
+
1. Update the `asherah` gem version in your Gemfile
|
|
192
|
+
2. No code changes required -- the API is compatible
|
|
193
|
+
3. Both read the same metastore tables -- no data migration required
|
|
194
|
+
|
|
195
|
+
## Performance
|
|
196
|
+
|
|
197
|
+
Benchmarked on Apple M4 Max, 64-byte payload, hot session cache:
|
|
198
|
+
|
|
199
|
+
| Operation | Latency |
|
|
200
|
+
|---|---|
|
|
201
|
+
| Encrypt | ~1,170 ns |
|
|
202
|
+
| Decrypt | ~1,110 ns |
|
|
203
|
+
|
|
204
|
+
## Configuration
|
|
205
|
+
|
|
206
|
+
### `setup` (hash style)
|
|
207
|
+
|
|
208
|
+
Keys are PascalCase strings matching the Asherah configuration format:
|
|
209
|
+
|
|
210
|
+
| Key | Type | Required | Description |
|
|
211
|
+
|---|---|---|---|
|
|
212
|
+
| `ServiceName` | `String` | Yes | Service identifier for key hierarchy |
|
|
213
|
+
| `ProductID` | `String` | Yes | Product identifier for key hierarchy |
|
|
214
|
+
| `Metastore` | `String` | Yes | `"rdbms"`, `"dynamodb"`, `"memory"` (testing) |
|
|
215
|
+
| `KMS` | `String` | Yes | `"static"` or `"aws"` |
|
|
216
|
+
| `ConnectionString` | `String` | No | RDBMS connection string |
|
|
217
|
+
| `DynamoDBEndpoint` | `String` | No | Custom DynamoDB endpoint |
|
|
218
|
+
| `DynamoDBRegion` | `String` | No | DynamoDB region — drives endpoint URL resolution and (when `DynamoDBSigningRegion` is unset) SigV4 signing |
|
|
219
|
+
| `DynamoDBSigningRegion` | `String` | No | SigV4 signing region. When set distinct from `DynamoDBRegion`, the URL is built from `DynamoDBRegion` but SigV4 signs as `DynamoDBSigningRegion` |
|
|
220
|
+
| `DynamoDBTableName` | `String` | No | DynamoDB table name |
|
|
221
|
+
| `RegionMap` | `Hash` | No | AWS KMS region-to-ARN map |
|
|
222
|
+
| `PreferredRegion` | `String` | No | Preferred AWS KMS region |
|
|
223
|
+
| `AwsProfileName` | `String` | No | AWS shared-credentials profile for KMS, DynamoDB, and Secrets Manager clients (native Rust SDK) |
|
|
224
|
+
| `EnableRegionSuffix` | `Boolean` | No | Append region suffix to key IDs |
|
|
225
|
+
| `EnableSessionCaching` | `Boolean` | No | Enable session caching (default: true) |
|
|
226
|
+
| `SessionCacheMaxSize` | `Integer` | No | Max cached sessions |
|
|
227
|
+
| `SessionCacheDuration` | `Integer` | No | Cache TTL in milliseconds |
|
|
228
|
+
| `ExpireAfter` | `Integer` | No | Key expiration in seconds |
|
|
229
|
+
| `CheckInterval` | `Integer` | No | Key check interval in seconds |
|
|
230
|
+
| `Verbose` | `Boolean` | No | Enable verbose logging (default: false) |
|
|
231
|
+
| `PoolMaxOpen` | `Integer` | No | Max open DB connections (default: 0 = unlimited) |
|
|
232
|
+
| `PoolMaxIdle` | `Integer` | No | Max idle connections to retain (default: 2) |
|
|
233
|
+
| `PoolMaxLifetime` | `Integer` | No | Max connection lifetime in seconds (default: 0 = unlimited) |
|
|
234
|
+
| `PoolMaxIdleTime` | `Integer` | No | Max idle time per connection in seconds (default: 0 = unlimited) |
|
|
235
|
+
|
|
236
|
+
For AWS KMS, DynamoDB, or Secrets Manager, when `AwsProfileName` is omitted the native Rust credential chain applies (including `AWS_PROFILE` and shared config under `~/.aws/`). Setting `AwsProfileName` / `aws_profile_name` selects a named profile explicitly.
|
|
237
|
+
|
|
238
|
+
### `configure` (block style)
|
|
239
|
+
|
|
240
|
+
Uses snake_case attribute accessors:
|
|
241
|
+
|
|
242
|
+
| Attribute | Maps to |
|
|
243
|
+
|---|---|
|
|
244
|
+
| `service_name` | `ServiceName` |
|
|
245
|
+
| `product_id` | `ProductID` |
|
|
246
|
+
| `metastore` | `Metastore` |
|
|
247
|
+
| `kms` | `KMS` |
|
|
248
|
+
| `connection_string` | `ConnectionString` |
|
|
249
|
+
| `dynamo_db_endpoint` | `DynamoDBEndpoint` |
|
|
250
|
+
| `dynamo_db_region` | `DynamoDBRegion` |
|
|
251
|
+
| `dynamo_db_signing_region` | `DynamoDBSigningRegion` |
|
|
252
|
+
| `dynamo_db_table_name` | `DynamoDBTableName` |
|
|
253
|
+
| `region_map` | `RegionMap` |
|
|
254
|
+
| `preferred_region` | `PreferredRegion` |
|
|
255
|
+
| `aws_profile_name` | `AwsProfileName` |
|
|
256
|
+
| `enable_region_suffix` | `EnableRegionSuffix` |
|
|
257
|
+
| `enable_session_caching` | `EnableSessionCaching` |
|
|
258
|
+
| `session_cache_max_size` | `SessionCacheMaxSize` |
|
|
259
|
+
| `session_cache_duration` | `SessionCacheDuration` |
|
|
260
|
+
| `expire_after` | `ExpireAfter` |
|
|
261
|
+
| `check_interval` | `CheckInterval` |
|
|
262
|
+
| `verbose` | `Verbose` |
|
|
263
|
+
| `pool_max_open` | `PoolMaxOpen` |
|
|
264
|
+
| `pool_max_idle` | `PoolMaxIdle` |
|
|
265
|
+
| `pool_max_lifetime` | `PoolMaxLifetime` |
|
|
266
|
+
| `pool_max_idle_time` | `PoolMaxIdleTime` |
|
|
267
|
+
|
|
268
|
+
## API Reference
|
|
269
|
+
|
|
270
|
+
### `Asherah` (module-level static API)
|
|
271
|
+
|
|
272
|
+
| Method | Description |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `setup(config_hash)` | Initialize with PascalCase config hash |
|
|
275
|
+
| `configure { \|c\| ... }` | Initialize with block-style snake_case config |
|
|
276
|
+
| `setup_async(config_hash, &block)` | Async `setup` in a Thread |
|
|
277
|
+
| `shutdown` | Release all resources and cached sessions |
|
|
278
|
+
| `shutdown_async(&block)` | Async `shutdown` in a Thread |
|
|
279
|
+
| `get_setup_status` | Returns `true` if initialized |
|
|
280
|
+
| `encrypt(partition, data)` | Encrypt bytes, returns DRR JSON bytes |
|
|
281
|
+
| `encrypt_string(partition, text)` | Encrypt string, returns DRR JSON string |
|
|
282
|
+
| `encrypt_async(partition, data, &block)` | Encrypt in a Thread |
|
|
283
|
+
| `decrypt(partition, drr)` | Decrypt DRR JSON bytes to plaintext |
|
|
284
|
+
| `decrypt_string(partition, drr)` | Decrypt DRR JSON string to plaintext string |
|
|
285
|
+
| `decrypt_async(partition, drr, &block)` | Decrypt in a Thread |
|
|
286
|
+
| `setenv(hash)` / `set_env(hash)` | Set environment variables |
|
|
287
|
+
|
|
288
|
+
### `Asherah::SessionFactory`
|
|
289
|
+
|
|
290
|
+
| Method | Description |
|
|
291
|
+
|---|---|
|
|
292
|
+
| `get_session(partition_id)` | Create a session for a partition |
|
|
293
|
+
| `close` | Release the factory |
|
|
294
|
+
| `closed?` | Returns `true` if closed |
|
|
295
|
+
|
|
296
|
+
### `Asherah::Session`
|
|
297
|
+
|
|
298
|
+
| Method | Description |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `encrypt_bytes(data)` | Encrypt bytes, returns DRR JSON bytes |
|
|
301
|
+
| `decrypt_bytes(json)` | Decrypt DRR JSON bytes to plaintext bytes |
|
|
302
|
+
| `encrypt_bytes_async(data)` | True async encrypt via Rust tokio |
|
|
303
|
+
| `decrypt_bytes_async(json)` | True async decrypt via Rust tokio |
|
|
304
|
+
| `close` | Release the session |
|
|
305
|
+
| `closed?` | Returns `true` if closed |
|
|
306
|
+
|
|
307
|
+
## Observability hooks
|
|
308
|
+
|
|
309
|
+
### Log hook
|
|
310
|
+
|
|
311
|
+
Asherah ships first-class stdlib `Logger` integration. The simplest way to
|
|
312
|
+
wire up logging is to hand it any Logger-compatible instance — stdlib
|
|
313
|
+
`Logger`, `ActiveSupport::Logger`, `SemanticLogger`, `Ougai`, etc. — and the
|
|
314
|
+
bridge dispatches each record via `Logger#add(severity, message, target)`
|
|
315
|
+
so the logger's own filter rules and formatters apply.
|
|
89
316
|
|
|
90
|
-
|
|
317
|
+
```ruby
|
|
318
|
+
require "logger"
|
|
319
|
+
log = Logger.new($stdout)
|
|
320
|
+
log.level = Logger::WARN
|
|
321
|
+
Asherah.set_log_hook(log)
|
|
91
322
|
|
|
323
|
+
# ...later
|
|
324
|
+
Asherah.clear_log_hook
|
|
92
325
|
```
|
|
93
|
-
|
|
94
|
-
|
|
326
|
+
|
|
327
|
+
For raw access pass a block; the event is a `Hash` with both a
|
|
328
|
+
`Logger::Severity` integer and a matching lowercase symbol:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
Asherah.set_log_hook do |event|
|
|
332
|
+
# event[:severity] => Logger::DEBUG | INFO | WARN | ERROR
|
|
333
|
+
# event[:level] => :debug | :info | :warn | :error (symbol, for case dispatch)
|
|
334
|
+
# event[:target] => "asherah::session"
|
|
335
|
+
# event[:message] => "..."
|
|
336
|
+
next if event[:severity] < Logger::WARN
|
|
337
|
+
warn "[asherah #{event[:level]}] #{event[:target]}: #{event[:message]}"
|
|
338
|
+
end
|
|
95
339
|
```
|
|
96
340
|
|
|
97
|
-
|
|
341
|
+
The Rust `log` crate has a TRACE level that stdlib `Logger` does not; Asherah
|
|
342
|
+
maps `trace` records to `Logger::DEBUG` so the value is still meaningful.
|
|
343
|
+
The block may fire from any thread (Rust tokio worker threads, DB driver
|
|
344
|
+
threads), so implementations must be thread-safe and should not block.
|
|
345
|
+
Exceptions raised from the callback are caught and silently swallowed —
|
|
346
|
+
propagating an exception across the FFI boundary is undefined behavior.
|
|
98
347
|
|
|
348
|
+
### Metrics hook
|
|
349
|
+
|
|
350
|
+
Receive timing observations (`:encrypt`, `:decrypt`, `:store`, `:load`) and
|
|
351
|
+
cache events (`:cache_hit`, `:cache_miss`, `:cache_stale`) via
|
|
352
|
+
`Asherah.set_metrics_hook`. Installing a hook implicitly enables the global
|
|
353
|
+
metrics gate; clearing it disables the gate.
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
Asherah.set_metrics_hook do |event|
|
|
357
|
+
case event[:type]
|
|
358
|
+
when :encrypt, :decrypt, :store, :load
|
|
359
|
+
# event[:duration_ns] is the elapsed time in nanoseconds, event[:name] is nil
|
|
360
|
+
Statsd.timing("asherah.#{event[:type]}", event[:duration_ns] / 1_000_000.0)
|
|
361
|
+
when :cache_hit, :cache_miss, :cache_stale
|
|
362
|
+
# event[:name] is the cache identifier, event[:duration_ns] is 0
|
|
363
|
+
Statsd.increment("asherah.#{event[:type]}.#{event[:name]}")
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# ...later
|
|
368
|
+
Asherah.clear_metrics_hook
|
|
369
|
+
```
|
|
99
370
|
|
|
100
|
-
|
|
371
|
+
| Event type | `:duration_ns` | `:name` |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| `:encrypt` | elapsed ns | `nil` |
|
|
374
|
+
| `:decrypt` | elapsed ns | `nil` |
|
|
375
|
+
| `:store` | elapsed ns | `nil` |
|
|
376
|
+
| `:load` | elapsed ns | `nil` |
|
|
377
|
+
| `:cache_hit` | `0` | cache identifier |
|
|
378
|
+
| `:cache_miss` | `0` | cache identifier |
|
|
379
|
+
| `:cache_stale` | `0` | cache identifier |
|
|
101
380
|
|
|
102
|
-
|
|
381
|
+
The same threading caveats apply as for the log hook — implementations must
|
|
382
|
+
be thread-safe and non-blocking, and exceptions are caught.
|
|
103
383
|
|
|
104
384
|
## License
|
|
105
385
|
|
|
106
|
-
|
|
386
|
+
Licensed under the Apache License, Version 2.0.
|
data/lib/asherah/config.rb
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "json"
|
|
4
4
|
|
|
5
5
|
module Asherah
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# @attr [String] region_map, List of key-value pairs in the form of REGION1=ARN1[,REGION2=ARN2] (required for aws kms)
|
|
18
|
-
# @attr [String] preferred_region, The preferred AWS region (required for aws kms)
|
|
19
|
-
# @attr [Integer] session_cache_max_size, The maximum number of sessions to cache
|
|
20
|
-
# @attr [Integer] session_cache_duration, The amount of time in seconds a session will remain cached
|
|
21
|
-
# @attr [Integer] expire_after, The amount of time in seconds a key is considered valid
|
|
22
|
-
# @attr [Integer] check_interval, The amount of time in seconds before cached keys are considered stale
|
|
23
|
-
# @attr [Boolean] enable_session_caching, Enable shared session caching
|
|
24
|
-
# @attr [Boolean] disable_zero_copy, Disable zero-copy FFI input buffers to prevent use-after-free from caller runtime
|
|
25
|
-
# @attr [Boolean] verbose, Enable verbose logging output
|
|
6
|
+
# Configuration class compatible with the canonical godaddy/asherah-ruby gem.
|
|
7
|
+
# Provides snake_case attr_accessors that map to PascalCase config keys
|
|
8
|
+
# expected by the Rust FFI layer.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# Asherah.configure do |config|
|
|
12
|
+
# config.service_name = "MyService"
|
|
13
|
+
# config.product_id = "MyProduct"
|
|
14
|
+
# config.kms = "static"
|
|
15
|
+
# config.metastore = "memory"
|
|
16
|
+
# end
|
|
26
17
|
class Config
|
|
27
18
|
MAPPING = {
|
|
28
19
|
service_name: :ServiceName,
|
|
@@ -34,83 +25,82 @@ module Asherah
|
|
|
34
25
|
sql_metastore_db_type: :SQLMetastoreDBType,
|
|
35
26
|
dynamo_db_endpoint: :DynamoDBEndpoint,
|
|
36
27
|
dynamo_db_region: :DynamoDBRegion,
|
|
28
|
+
dynamo_db_signing_region: :DynamoDBSigningRegion,
|
|
37
29
|
dynamo_db_table_name: :DynamoDBTableName,
|
|
38
30
|
enable_region_suffix: :EnableRegionSuffix,
|
|
39
31
|
region_map: :RegionMap,
|
|
40
32
|
preferred_region: :PreferredRegion,
|
|
33
|
+
aws_profile_name: :AwsProfileName,
|
|
41
34
|
session_cache_max_size: :SessionCacheMaxSize,
|
|
42
35
|
session_cache_duration: :SessionCacheDuration,
|
|
43
36
|
enable_session_caching: :EnableSessionCaching,
|
|
44
37
|
disable_zero_copy: :DisableZeroCopy,
|
|
45
38
|
expire_after: :ExpireAfter,
|
|
46
39
|
check_interval: :CheckInterval,
|
|
47
|
-
verbose: :Verbose
|
|
40
|
+
verbose: :Verbose,
|
|
41
|
+
# Connection pool
|
|
42
|
+
pool_max_open: :PoolMaxOpen,
|
|
43
|
+
pool_max_idle: :PoolMaxIdle,
|
|
44
|
+
pool_max_lifetime: :PoolMaxLifetime,
|
|
45
|
+
pool_max_idle_time: :PoolMaxIdleTime,
|
|
46
|
+
# KMS: AWS
|
|
47
|
+
kms_key_id: :KmsKeyId,
|
|
48
|
+
# KMS: AWS Secrets Manager
|
|
49
|
+
secrets_manager_secret_id: :SecretsManagerSecretId,
|
|
50
|
+
# KMS: HashiCorp Vault Transit
|
|
51
|
+
vault_addr: :VaultAddr,
|
|
52
|
+
vault_token: :VaultToken,
|
|
53
|
+
vault_auth_method: :VaultAuthMethod,
|
|
54
|
+
vault_auth_role: :VaultAuthRole,
|
|
55
|
+
vault_auth_mount: :VaultAuthMount,
|
|
56
|
+
vault_approle_role_id: :VaultApproleRoleId,
|
|
57
|
+
vault_approle_secret_id: :VaultApproleSecretId,
|
|
58
|
+
vault_client_cert: :VaultClientCert,
|
|
59
|
+
vault_client_key: :VaultClientKey,
|
|
60
|
+
vault_k8s_token_path: :VaultK8sTokenPath,
|
|
61
|
+
vault_transit_key: :VaultTransitKey,
|
|
62
|
+
vault_transit_mount: :VaultTransitMount,
|
|
48
63
|
}.freeze
|
|
49
64
|
|
|
50
|
-
KMS_TYPES = [
|
|
51
|
-
METASTORE_TYPES = [
|
|
52
|
-
SQL_METASTORE_DB_TYPES = [
|
|
65
|
+
KMS_TYPES = ["static", "aws", "vault", "vault-transit", "secrets-manager", "test-debug-static"].freeze
|
|
66
|
+
METASTORE_TYPES = ["rdbms", "dynamodb", "memory", "test-debug-memory"].freeze
|
|
67
|
+
SQL_METASTORE_DB_TYPES = ["mysql", "postgres", "oracle"].freeze
|
|
53
68
|
|
|
54
69
|
attr_accessor(*MAPPING.keys)
|
|
55
70
|
|
|
56
71
|
def validate!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
validate_metastore
|
|
61
|
-
validate_sql_metastore_db_type
|
|
62
|
-
validate_kms_attributes
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def to_json(*args)
|
|
66
|
-
config = {}.tap do |c|
|
|
67
|
-
MAPPING.each_pair do |our_key, their_key|
|
|
68
|
-
value = public_send(our_key)
|
|
69
|
-
c[their_key] = value unless value.nil?
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
JSON.generate(config, *args)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
def validate_service_name
|
|
79
|
-
raise Error::ConfigError, 'config.service_name not set' if service_name.nil?
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def validate_product_id
|
|
83
|
-
raise Error::ConfigError, 'config.product_id not set' if product_id.nil?
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def validate_kms
|
|
87
|
-
raise Error::ConfigError, 'config.kms not set' if kms.nil?
|
|
72
|
+
raise Error::ConfigError, "config.service_name not set" if service_name.nil?
|
|
73
|
+
raise Error::ConfigError, "config.product_id not set" if product_id.nil?
|
|
74
|
+
raise Error::ConfigError, "config.kms not set" if kms.nil?
|
|
88
75
|
unless KMS_TYPES.include?(kms)
|
|
89
|
-
raise Error::ConfigError, "config.kms must be one of these: #{KMS_TYPES.join(
|
|
76
|
+
raise Error::ConfigError, "config.kms must be one of these: #{KMS_TYPES.join(", ")}"
|
|
90
77
|
end
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def validate_metastore
|
|
94
|
-
raise Error::ConfigError, 'config.metastore not set' if metastore.nil?
|
|
78
|
+
raise Error::ConfigError, "config.metastore not set" if metastore.nil?
|
|
95
79
|
unless METASTORE_TYPES.include?(metastore)
|
|
96
|
-
raise Error::ConfigError, "config.metastore must be one of these: #{METASTORE_TYPES.join(
|
|
80
|
+
raise Error::ConfigError, "config.metastore must be one of these: #{METASTORE_TYPES.join(", ")}"
|
|
81
|
+
end
|
|
82
|
+
if sql_metastore_db_type && !SQL_METASTORE_DB_TYPES.include?(sql_metastore_db_type)
|
|
83
|
+
raise Error::ConfigError, "config.sql_metastore_db_type must be one of these: #{SQL_METASTORE_DB_TYPES.join(", ")}"
|
|
84
|
+
end
|
|
85
|
+
if kms == "aws"
|
|
86
|
+
raise Error::ConfigError, "config.region_map not set" if region_map.nil?
|
|
87
|
+
raise Error::ConfigError, "config.region_map must be a Hash" unless region_map.is_a?(Hash)
|
|
88
|
+
raise Error::ConfigError, "config.preferred_region not set" if preferred_region.nil?
|
|
97
89
|
end
|
|
98
90
|
end
|
|
99
91
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
unless SQL_METASTORE_DB_TYPES.include?(sql_metastore_db_type)
|
|
104
|
-
raise Error::ConfigError,
|
|
105
|
-
"config.sql_metastore_db_type must be one of these: #{SQL_METASTORE_DB_TYPES.join(', ')}"
|
|
106
|
-
end
|
|
92
|
+
def to_json(*args)
|
|
93
|
+
JSON.generate(to_h, *args)
|
|
107
94
|
end
|
|
108
95
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
96
|
+
# Convert to the PascalCase Hash expected by Asherah.setup
|
|
97
|
+
def to_h
|
|
98
|
+
hash = {}
|
|
99
|
+
MAPPING.each_pair do |attr, key|
|
|
100
|
+
value = public_send(attr)
|
|
101
|
+
hash[key] = value unless value.nil?
|
|
102
|
+
end
|
|
103
|
+
hash
|
|
114
104
|
end
|
|
115
105
|
end
|
|
116
106
|
end
|