asherah 0.9.1-x86_64-linux → 0.10.0-x86_64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 662ff3d2213a16eabff46710628a39ac1572130702e20c126004a1b623ba13e4
4
- data.tar.gz: 909dc57d7943cb246c4ba6e713d19d48047b15015582c45483a86e52c5e39108
3
+ metadata.gz: 2b80d9aa02bdcf398d498d5252ca8ee3ac0bb336821afb3676bde3b2ac91a045
4
+ data.tar.gz: 1a6078f020a6cf4896b0f804b11178e3f7a0864e9d29ef6c18f64163145b7597
5
5
  SHA512:
6
- metadata.gz: 7292acfe8e7ecf0f70de7ca18eba3217f06654aef4315873036d23f0f0dbba2aa31d292e3a24aefbcb6bb655ba01af6737d7a2ec60f77838b567d1557fbe7ac1
7
- data.tar.gz: ae8c479637058cb1ddefa054e10f4fd95ccd33112475c33e2ca77cd7fbae7f8278608db46fa7c5f1e18a1a9bbc9186240461556f0ed3e90ca36b9f913207dd24
6
+ metadata.gz: 1d343cdc302bce582fa3ebb804dd8824dce31f7258b9147138cb7ed1c7ce8c77ead246c160ed27de89f5718058e46d3bbfa33616b181ff26cb8e5e482a1d6bbe
7
+ data.tar.gz: ad255a561b58fc3b7ec4921c98dfd3d824c3f179aa92e4afcec611719a1a6ca8ef3fbd28dd15868fa0929adf8fae73e4fa6d1cab5e91bcefce2a00946f2fd4c8
data/README.md CHANGED
@@ -1,106 +1,386 @@
1
- # Asherah
1
+ # asherah
2
2
 
3
- Asherah is a Ruby FFI wrapper around the Rust version of [Asherah](https://github.com/godaddy/asherah-ffi) application-layer encryption SDK. Asherah provides advanced encryption features and defense in depth against compromise. It uses a technique known as "envelope encryption" and supports cloud-agnostic data storage and key management.
3
+ Ruby bindings for [Asherah](https://github.com/godaddy/asherah-ffi) envelope encryption with automatic key rotation.
4
4
 
5
- Check out the following documentation to get more familiar with the concepts and configuration options:
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
- Add this line to your application's Gemfile:
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
- ```bash
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
- Or install it yourself as:
21
+ ## Documentation
30
22
 
31
- ```bash
32
- gem install asherah
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
- ## Usage
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
- Configure Asherah:
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.kms = 'static'
42
- config.metastore = 'memory'
43
- config.service_name = 'service'
44
- config.product_id = 'product'
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
- See [config.rb](lib/asherah/config.rb) for all available configuration options.
73
+ ## Session-Based API
49
74
 
50
- Encrypt some data for a `partition_id`
75
+ For direct control over session lifecycle, use `SessionFactory` and `Session`:
51
76
 
52
77
  ```ruby
53
- partition_id = 'user_1'
54
- data = 'PII data'
55
- data_row_record_json = Asherah.encrypt(partition_id, data)
56
- puts data_row_record_json
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
- Decrypt `data_row_record_json`
99
+ Or via the static API's internal factory (the typical pattern):
60
100
 
61
101
  ```ruby
62
- decrypted_data = Asherah.decrypt(partition_id, data_row_record_json)
63
- puts decrypted_data
64
- ```
102
+ Asherah.setup("ServiceName" => "my-service", "ProductID" => "my-product",
103
+ "Metastore" => "memory", "KMS" => "static") # testing only
65
104
 
66
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
109
+ Asherah.shutdown
110
+ ```
69
111
 
70
- For tests requiring secrets (AWS KMS, database credentials), copy `.env.secrets.example` to `.env.secrets` and fill in the required values. The `.env.secrets` file is already in `.gitignore` to prevent accidental commits.
112
+ ## Async API
71
113
 
72
- ### Cross-Language Tests
114
+ ### Session-level async (true async via Rust tokio)
73
115
 
74
- Cross-language tests verify that data encrypted with the Rust implementation can be decrypted with the Ruby implementation and vice versa.
116
+ The session's async methods dispatch work to Rust's tokio runtime and receive results via FFI callbacks:
75
117
 
76
- **Prerequisites:**
77
- - MySQL running locally
78
- - Go 1.24+ installed
118
+ ```ruby
119
+ session = factory.get_session("partition-id")
79
120
 
80
- **Running the tests:**
121
+ ct = session.encrypt_bytes_async(data)
122
+ pt = session.decrypt_bytes_async(ct)
81
123
 
82
- ```bash
83
- TEST_DB_PASSWORD=pass bin/cross-language-test.sh
124
+ session.close
84
125
  ```
85
126
 
86
- See `bin/cross-language-test.sh` for available environment variables and their defaults.
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
- To install this gem onto your local machine, run `rake install`.
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
- To release a new version, update the version number in `version.rb`, create and push a version tag:
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
- git tag -a v$(rake version) -m "Version $(rake version)"
94
- git push origin v$(rake version)
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
- And then create a release in Github with title `echo "Version $(rake version)"` that will trigger `.github/workflows/publish.yml` workflow and push the `.gem` file to [rubygems.org](https://rubygems.org):
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
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/godaddy/asherah-ruby.
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
- The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
386
+ Licensed under the Apache License, Version 2.0.
@@ -1,28 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require "json"
4
4
 
5
5
  module Asherah
6
- # @attr [String] service_name, The name of this service
7
- # @attr [String] product_id, The name of the product that owns this service
8
- # @attr [String] kms, The master key management service (static or aws)
9
- # @attr [String] metastore, The type of metastore for persisting keys (rdbms, dynamodb, memory)
10
- # @attr [String] connection_string, The database connection string (required when metastore is rdbms)
11
- # @attr [String] replica_read_consistency, For Aurora sessions using write forwarding (eventual, global, session)
12
- # @attr [String] sql_metastore_db_type, Which SQL driver to use (mysql, postgres, oracle), defaults to mysql
13
- # @attr [String] dynamo_db_endpoint, An optional endpoint URL (for dynamodb metastore)
14
- # @attr [String] dynamo_db_region, The AWS region for DynamoDB requests (for dynamodb metastore)
15
- # @attr [String] dynamo_db_table_name, The table name for DynamoDB (for dynamodb metastore)
16
- # @attr [Boolean] enable_region_suffix, Configure the metastore to use regional suffixes (for dynamodb metastore)
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 = ['static', 'aws', 'test-debug-static'].freeze
51
- METASTORE_TYPES = ['rdbms', 'dynamodb', 'memory', 'test-debug-memory'].freeze
52
- SQL_METASTORE_DB_TYPES = ['mysql', 'postgres', 'oracle'].freeze
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
- validate_service_name
58
- validate_product_id
59
- validate_kms
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
- end
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 validate_sql_metastore_db_type
101
- return if sql_metastore_db_type.nil?
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
- def validate_kms_attributes
110
- return unless kms == 'aws'
111
- raise Error::ConfigError, 'config.region_map not set' if region_map.nil?
112
- raise Error::ConfigError, 'config.region_map must be a Hash' unless region_map.is_a?(Hash)
113
- raise Error::ConfigError, 'config.preferred_region not set' if preferred_region.nil?
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