secvault 2.7.1 → 3.1.0
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 +60 -52
- data/lib/secvault/rails_secrets.rb +41 -38
- data/lib/secvault/railtie.rb +130 -10
- data/lib/secvault/secrets.rb +2 -3
- data/lib/secvault/version.rb +1 -1
- data/lib/secvault.rb +191 -176
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d57e110e287dd498f1751e5d823283c7f673c5f7511fca743772bec1ba25c5fc
|
|
4
|
+
data.tar.gz: b4c48354b471bc32634eb40dc15a99869e86825aae010f9f338ca4ab91850d66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c1f2e1f452cfca7bbcc34117a32b05d252589d156b59175158577548f9158628bf1d0f01ace21714b3f8cf7fd8de195769ecf0d60b6d11915d4ca10163070e3
|
|
7
|
+
data.tar.gz: 1b5ffc0246423e154b2c0e12799e6d11370c8fa242eed3bec9f902e98df9119e70d0ae1da27f02c206a62a47b839c8e0788f35afd0d5a5cca7fe360739c66ced
|
data/README.md
CHANGED
|
@@ -1,101 +1,109 @@
|
|
|
1
1
|
# Secvault
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Simple YAML secrets management for Rails. Uses standard YAML anchors for sharing configuration.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **Rails 7.2+**: Automatic setup (drop-in replacement for removed functionality)
|
|
8
|
-
- **Rails 7.1**: Manual setup required
|
|
9
|
-
- **Rails 8.0+**: Full compatibility
|
|
5
|
+
[](https://rubygems.org/gems/secvault)
|
|
10
6
|
|
|
11
7
|
## Installation
|
|
12
8
|
|
|
13
9
|
```ruby
|
|
14
|
-
# Gemfile
|
|
15
10
|
gem 'secvault'
|
|
16
11
|
```
|
|
17
12
|
|
|
18
|
-
##
|
|
13
|
+
## Usage
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
**1. Add to initializer:**
|
|
16
|
+
```ruby
|
|
17
|
+
# config/initializers/secvault.rb
|
|
18
|
+
Secvault.start!
|
|
19
|
+
```
|
|
21
20
|
|
|
21
|
+
**2. Create secrets file:**
|
|
22
22
|
```yaml
|
|
23
|
+
# config/secrets.yml
|
|
24
|
+
defaults: &defaults
|
|
25
|
+
app_name: "MyApp"
|
|
26
|
+
|
|
23
27
|
development:
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
<<: *defaults
|
|
29
|
+
secret_key_base: "dev_secret"
|
|
30
|
+
api_key: "dev_key"
|
|
26
31
|
|
|
27
32
|
production:
|
|
33
|
+
<<: *defaults
|
|
34
|
+
secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
|
|
28
35
|
api_key: <%= ENV['API_KEY'] %>
|
|
29
|
-
database_url: <%= ENV['DATABASE_URL'] %>
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
**3. Use in your app:**
|
|
34
39
|
```ruby
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
Secvault.secrets.api_key
|
|
41
|
+
Secvault.secrets.app_name
|
|
37
42
|
```
|
|
38
43
|
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
Load and merge multiple secrets files:
|
|
44
|
+
## Options
|
|
42
45
|
|
|
43
46
|
```ruby
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
Secvault.start!(
|
|
48
|
+
files: ['config/secrets.yml'], # Files to load (later files override earlier ones)
|
|
49
|
+
integrate_with_rails: false, # Add Rails.application.secrets
|
|
50
|
+
set_secret_key_base: true, # Auto-set Rails.application.config.secret_key_base from secrets
|
|
51
|
+
hot_reload: true, # Auto-reload in development
|
|
52
|
+
logger: true # Log loading activity
|
|
53
|
+
)
|
|
50
54
|
```
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Manual API
|
|
55
|
-
|
|
56
|
+
**Multiple files:**
|
|
56
57
|
```ruby
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Load default config/secrets.yml
|
|
61
|
-
secrets = Rails::Secrets.load(env: 'production')
|
|
62
|
-
|
|
63
|
-
# Check if active
|
|
64
|
-
Secvault.active? # => true/false
|
|
58
|
+
# Later files override earlier ones
|
|
59
|
+
Secvault.start!(files: ['secrets.yml', 'local.yml'])
|
|
65
60
|
```
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
**Rails integration:**
|
|
63
|
+
```ruby
|
|
64
|
+
Secvault.start!(integrate_with_rails: true)
|
|
65
|
+
Rails.application.secrets.api_key # Now available
|
|
66
|
+
```
|
|
70
67
|
|
|
68
|
+
**Secret key base:**
|
|
71
69
|
```ruby
|
|
72
|
-
#
|
|
73
|
-
|
|
70
|
+
# If your secrets.yml has secret_key_base, it's automatically set
|
|
71
|
+
# This replaces the need for Rails.application.config.secret_key_base
|
|
72
|
+
Secvault.start!(set_secret_key_base: true) # Default behavior
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
## ERB Templating
|
|
77
75
|
|
|
78
|
-
|
|
76
|
+
## Advanced
|
|
79
77
|
|
|
78
|
+
**ERB templating:**
|
|
80
79
|
```yaml
|
|
81
80
|
production:
|
|
82
81
|
api_key: <%= ENV['API_KEY'] %>
|
|
83
82
|
pool_size: <%= ENV.fetch('DB_POOL', '5').to_i %>
|
|
84
|
-
features:
|
|
85
|
-
enabled: <%= ENV.fetch('FEATURES_ON', 'false') == 'true' %>
|
|
86
|
-
hosts: <%= ENV.fetch('ALLOWED_HOSTS', 'localhost').split(',') %>
|
|
87
83
|
```
|
|
88
84
|
|
|
89
|
-
|
|
85
|
+
**YAML anchors for sharing:**
|
|
86
|
+
```yaml
|
|
87
|
+
defaults: &defaults
|
|
88
|
+
app_name: "MyApp"
|
|
89
|
+
timeout: 30
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
development:
|
|
92
|
+
<<: *defaults
|
|
93
|
+
debug: true
|
|
92
94
|
|
|
95
|
+
production:
|
|
96
|
+
<<: *defaults
|
|
97
|
+
timeout: 10 # Override specific values
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Development helpers:**
|
|
93
101
|
```ruby
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
Rails.application.reload_secrets!
|
|
102
|
+
reload_secrets! # Reload files
|
|
103
|
+
Secvault.active? # Check status
|
|
97
104
|
```
|
|
98
105
|
|
|
106
|
+
|
|
99
107
|
## License
|
|
100
108
|
|
|
101
109
|
MIT
|
|
@@ -1,54 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Secvault
|
|
4
|
-
# Rails::Secrets compatibility
|
|
4
|
+
# Rails::Secrets compatibility class
|
|
5
5
|
# Provides the classic Rails::Secrets interface for backwards compatibility
|
|
6
|
-
# This replicates the Rails < 7.2 Rails::Secrets
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
# This replicates the Rails < 7.2 Rails::Secrets class functionality
|
|
7
|
+
class RailsSecrets
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :root
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
11
|
+
# Parse secrets from one or more YAML files
|
|
12
|
+
#
|
|
13
|
+
# Supports:
|
|
14
|
+
# - ERB templating for environment variables
|
|
15
|
+
# - Environment-specific sections (YAML anchors handle sharing)
|
|
16
|
+
# - Multiple files (merged in order)
|
|
17
|
+
# - Deep symbolized keys
|
|
18
|
+
#
|
|
19
|
+
# Examples:
|
|
20
|
+
# # Single file
|
|
21
|
+
# Rails::Secrets.parse(['config/secrets.yml'], env: 'development')
|
|
22
|
+
#
|
|
23
|
+
# # Multiple files (merged in order)
|
|
24
|
+
# Rails::Secrets.parse([
|
|
25
|
+
# 'config/secrets.yml',
|
|
26
|
+
# 'config/secrets.local.yml'
|
|
27
|
+
# ], env: 'development')
|
|
28
|
+
#
|
|
29
|
+
# # Load default config/secrets.yml
|
|
30
|
+
# Rails::Secrets.load # uses current Rails.env
|
|
31
|
+
# Rails::Secrets.load(env: 'production')
|
|
32
|
+
def parse(paths, env:)
|
|
33
|
+
Secvault::Secrets.parse(paths, env: env.to_s)
|
|
34
|
+
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
# Load secrets from the default config/secrets.yml file
|
|
37
|
+
def load(env: Rails.env)
|
|
38
|
+
secrets_path = Rails.root.join("config/secrets.yml")
|
|
39
|
+
parse([secrets_path], env: env)
|
|
40
|
+
end
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
# Backward compatibility aliases (deprecated)
|
|
43
|
+
alias_method :parse_default, :load
|
|
44
|
+
alias_method :read, :load
|
|
45
|
+
end
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
#
|
|
49
|
+
# Replace Rails::Secrets interface for backwards compatibility
|
|
49
50
|
# Works consistently across all Rails versions with warning suppression
|
|
50
51
|
if defined?(Rails)
|
|
51
52
|
module Rails
|
|
53
|
+
# Remove existing constant to avoid warnings
|
|
54
|
+
remove_const(:Secrets) if const_defined?(:Secrets, false)
|
|
52
55
|
Secrets = Secvault::RailsSecrets
|
|
53
56
|
end
|
|
54
57
|
end
|
data/lib/secvault/railtie.rb
CHANGED
|
@@ -2,34 +2,154 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails/railtie"
|
|
4
4
|
|
|
5
|
+
# Extremely early hook to set up Rails.application.secrets before Application class is defined
|
|
6
|
+
if defined?(Rails)
|
|
7
|
+
# Set up a robust Rails.application with secrets support
|
|
8
|
+
unless Rails.respond_to?(:application) && Rails.application.respond_to?(:secrets)
|
|
9
|
+
# Create a minimal application-like object
|
|
10
|
+
temp_app = Object.new
|
|
11
|
+
|
|
12
|
+
# Add secrets method with default empty secrets that include needed encryption keys
|
|
13
|
+
temp_app.define_singleton_method(:secrets) do
|
|
14
|
+
@secrets ||= begin
|
|
15
|
+
secrets = ActiveSupport::OrderedOptions.new
|
|
16
|
+
|
|
17
|
+
# Add empty encryption section to prevent NoMethodError
|
|
18
|
+
secrets.encryption = {
|
|
19
|
+
primary_key: nil,
|
|
20
|
+
deterministic_key: nil,
|
|
21
|
+
key_derivation_salt: nil
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
secrets
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Set up Rails.application if it doesn't exist
|
|
29
|
+
Rails.define_singleton_method(:application) { temp_app } unless Rails.respond_to?(:application)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
5
33
|
module Secvault
|
|
6
34
|
class Railtie < Rails::Railtie
|
|
7
35
|
railtie_name :secvault
|
|
8
36
|
|
|
37
|
+
# Hook to set up early secrets access before application configuration
|
|
38
|
+
config.before_configuration do |app|
|
|
39
|
+
Secvault::EarlyLoader.setup_early_secrets(app)
|
|
40
|
+
end
|
|
41
|
+
|
|
9
42
|
initializer "secvault.initialize", before: :load_environment_hook do |app|
|
|
10
43
|
Secvault::Secrets.setup(app)
|
|
11
44
|
end
|
|
45
|
+
end
|
|
12
46
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
47
|
+
# Early loader class to handle secrets before application configuration
|
|
48
|
+
class EarlyLoader
|
|
49
|
+
class << self
|
|
50
|
+
def setup_early_secrets(app)
|
|
51
|
+
puts "[Secvault Debug] setup_early_secrets called" unless Rails.env.production?
|
|
52
|
+
|
|
53
|
+
if Rails.application.respond_to?(:secrets) && !Rails.application.secrets.empty?
|
|
54
|
+
puts "[Secvault Debug] Secrets already exist, skipping early load" unless Rails.env.production?
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Look for Secvault configuration in the app
|
|
59
|
+
secrets_config = find_secvault_config(app)
|
|
60
|
+
puts "[Secvault Debug] Found config: #{secrets_config&.keys}" unless Rails.env.production?
|
|
61
|
+
return unless secrets_config
|
|
16
62
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
63
|
+
begin
|
|
64
|
+
# Load secrets using the configuration found
|
|
65
|
+
all_secrets = Secvault::Secrets.parse(secrets_config[:files], env: Rails.env)
|
|
66
|
+
puts "[Secvault Debug] Loaded secrets keys: #{all_secrets.keys}" unless Rails.env.production?
|
|
21
67
|
|
|
22
|
-
|
|
68
|
+
# Set up Rails.application.secrets immediately
|
|
23
69
|
Rails.application.define_singleton_method(:secrets) do
|
|
24
70
|
@secrets ||= begin
|
|
25
71
|
current_secrets = ActiveSupport::OrderedOptions.new
|
|
26
|
-
|
|
27
|
-
current_secrets.
|
|
72
|
+
current_secrets.merge!(all_secrets)
|
|
73
|
+
puts "[Secvault Debug] Returning secrets with encryption: #{current_secrets.encryption}" unless Rails.env.production?
|
|
28
74
|
current_secrets
|
|
29
75
|
end
|
|
30
76
|
end
|
|
77
|
+
|
|
78
|
+
# Test the secrets immediately
|
|
79
|
+
test_encryption = Rails.application.secrets.encryption
|
|
80
|
+
puts "[Secvault Debug] Test access - encryption: #{test_encryption.class} - #{test_encryption}" unless Rails.env.production?
|
|
81
|
+
|
|
82
|
+
Rails.logger&.info "[Secvault] Early secrets loaded from #{secrets_config[:files].size} files" unless Rails.env.production?
|
|
83
|
+
rescue => e
|
|
84
|
+
Rails.logger&.warn "[Secvault] Failed to load early secrets: #{e.message}"
|
|
31
85
|
end
|
|
32
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def find_secvault_config(app)
|
|
91
|
+
# Look for Secvault configuration in various locations
|
|
92
|
+
config_locations = [
|
|
93
|
+
app.root.join("config/initializers/secvault.rb"),
|
|
94
|
+
app.root.join("config/secvault.rb")
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
config_locations.each do |config_file|
|
|
98
|
+
next unless config_file.exist?
|
|
99
|
+
|
|
100
|
+
config = parse_secvault_config(config_file)
|
|
101
|
+
return config if config
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Fallback to default configuration
|
|
105
|
+
default_files = [app.root.join("config/secrets.yml")]
|
|
106
|
+
|
|
107
|
+
# Check if neeto-commons-backend is available for default config
|
|
108
|
+
if defined?(NeetoCommonsBackend) && NeetoCommonsBackend.respond_to?(:shared_secrets_file)
|
|
109
|
+
default_files.unshift(NeetoCommonsBackend.shared_secrets_file)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Only return default if at least one file exists
|
|
113
|
+
existing_files = default_files.select(&:exist?)
|
|
114
|
+
return {files: existing_files} if existing_files.any?
|
|
115
|
+
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def parse_secvault_config(config_file)
|
|
120
|
+
# Read the configuration file and extract Secvault.start! parameters
|
|
121
|
+
content = config_file.read
|
|
122
|
+
|
|
123
|
+
# Look for Secvault.start! calls
|
|
124
|
+
if /Secvault\.start!\s*\(/m.match?(content)
|
|
125
|
+
# Try to extract the files parameter using a simple regex
|
|
126
|
+
files_match = content.match(/files:\s*\[(.*?)\]/m)
|
|
127
|
+
if files_match
|
|
128
|
+
# Parse the files array (basic string parsing)
|
|
129
|
+
files_content = files_match[1]
|
|
130
|
+
files = []
|
|
131
|
+
|
|
132
|
+
# Handle various file specification patterns
|
|
133
|
+
files_content.scan(/["'](.*?)["']|([A-Za-z_][\w.]*\([^)]*\))/) do |quoted, method_call|
|
|
134
|
+
if quoted
|
|
135
|
+
files << Rails.root.join(quoted.strip)
|
|
136
|
+
elsif method_call
|
|
137
|
+
# Handle method calls like NeetoCommonsBackend.shared_secrets_file
|
|
138
|
+
if method_call.include?("NeetoCommonsBackend.shared_secrets_file") && defined?(NeetoCommonsBackend)
|
|
139
|
+
files << NeetoCommonsBackend.shared_secrets_file
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return {files: files.compact} if files.any?
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
nil
|
|
149
|
+
rescue => e
|
|
150
|
+
Rails.logger&.warn "[Secvault] Failed to parse config file #{config_file}: #{e.message}"
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
33
153
|
end
|
|
34
154
|
end
|
|
35
155
|
end
|
data/lib/secvault/secrets.rb
CHANGED
|
@@ -56,7 +56,7 @@ module Secvault
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Classic Rails::Secrets.parse implementation
|
|
59
|
-
# Parses plain YAML secrets files
|
|
59
|
+
# Parses plain YAML secrets files for specific environment
|
|
60
60
|
def parse(paths, env:)
|
|
61
61
|
paths.each_with_object({}) do |path, all_secrets|
|
|
62
62
|
# Handle string paths by converting to Pathname
|
|
@@ -72,8 +72,7 @@ module Secvault
|
|
|
72
72
|
|
|
73
73
|
secrets ||= {}
|
|
74
74
|
|
|
75
|
-
#
|
|
76
|
-
all_secrets.deep_merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"]
|
|
75
|
+
# Only load environment-specific section (YAML anchors handle sharing)
|
|
77
76
|
all_secrets.deep_merge!(secrets[env].deep_symbolize_keys) if secrets[env]
|
|
78
77
|
end
|
|
79
78
|
end
|
data/lib/secvault/version.rb
CHANGED
data/lib/secvault.rb
CHANGED
|
@@ -15,7 +15,7 @@ loader.setup
|
|
|
15
15
|
#
|
|
16
16
|
# Secvault restores the classic Rails secrets.yml functionality using simple,
|
|
17
17
|
# plain YAML files for environment-specific secrets management. Works consistently
|
|
18
|
-
# across all Rails versions
|
|
18
|
+
# across all Rails versions.
|
|
19
19
|
#
|
|
20
20
|
# ## Rails Version Support:
|
|
21
21
|
# - Rails 7.1+: Full compatibility with automatic setup
|
|
@@ -26,18 +26,20 @@ loader.setup
|
|
|
26
26
|
# Add this to an initializer:
|
|
27
27
|
#
|
|
28
28
|
# # config/initializers/secvault.rb
|
|
29
|
-
# Secvault.
|
|
29
|
+
# Secvault.start!
|
|
30
30
|
#
|
|
31
31
|
# ## Usage:
|
|
32
32
|
# Rails.application.secrets.api_key
|
|
33
33
|
# Rails.application.secrets.oauth_settings[:google_client_id]
|
|
34
|
+
# Secvault.secrets.your_key # Direct access
|
|
34
35
|
# Rails::Secrets.load(env: 'development') # Load default config/secrets.yml
|
|
35
36
|
# Rails::Secrets.parse(['custom.yml'], env: Rails.env) # Parse custom files
|
|
36
37
|
#
|
|
37
38
|
# ## Getting Started:
|
|
38
39
|
# 1. Create config/secrets.yml with your secrets
|
|
39
|
-
# 2.
|
|
40
|
-
# 3.
|
|
40
|
+
# 2. Call Secvault.start! in an initializer
|
|
41
|
+
# 3. Use Rails.application.secrets.your_secret in your app
|
|
42
|
+
# 4. For production, use environment variables with ERB syntax
|
|
41
43
|
#
|
|
42
44
|
# @see https://github.com/unnitallman/secvault
|
|
43
45
|
module Secvault
|
|
@@ -63,122 +65,135 @@ module Secvault
|
|
|
63
65
|
defined?(Rails) && Rails::Secrets == Secvault::RailsSecrets
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
# Early setup method for use in config/application.rb before other configuration
|
|
69
|
+
# This ensures Rails.application has secrets available during application class definition
|
|
70
|
+
def setup_early_application_secrets!(files: nil, application_class: nil)
|
|
71
|
+
return false unless defined?(Rails)
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
# Default files if not provided
|
|
74
|
+
files ||= begin
|
|
75
|
+
default_files = ["config/secrets.yml"]
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# Usage in an initializer:
|
|
78
|
-
# Secvault.setup!
|
|
79
|
-
# Secvault.setup!(suppress_warnings: false)
|
|
80
|
-
#
|
|
81
|
-
# This will:
|
|
82
|
-
# 1. Set up Rails::Secrets with Secvault implementation
|
|
83
|
-
# 2. Replace Rails.application.secrets with Secvault-powered functionality
|
|
84
|
-
# 3. Load secrets from config/secrets.yml automatically
|
|
85
|
-
# 4. Suppress Rails deprecation warnings about secrets (default: true)
|
|
86
|
-
# 5. Set Rails.application.config.secret_key_base from secrets (default: true)
|
|
87
|
-
def setup!(suppress_warnings: true, set_secret_key_base: true)
|
|
88
|
-
# Override native Rails::Secrets
|
|
89
|
-
Rails.send(:remove_const, :Secrets) if defined?(Rails::Secrets)
|
|
90
|
-
Rails.const_set(:Secrets, Secvault::RailsSecrets)
|
|
77
|
+
# Add neeto-commons-backend file if available
|
|
78
|
+
if defined?(NeetoCommonsBackend) && NeetoCommonsBackend.respond_to?(:shared_secrets_file)
|
|
79
|
+
default_files.unshift(NeetoCommonsBackend.shared_secrets_file)
|
|
80
|
+
end
|
|
91
81
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# Suppress Rails deprecation warnings about secrets if requested
|
|
95
|
-
suppress_secrets_deprecation_warning! if suppress_warnings
|
|
82
|
+
default_files
|
|
83
|
+
end
|
|
96
84
|
|
|
97
|
-
|
|
85
|
+
# Create a temporary Rails.application if it doesn't exist
|
|
86
|
+
unless Rails.respond_to?(:application) && Rails.application
|
|
87
|
+
# Create a temporary application-like object with secrets
|
|
88
|
+
temp_app = Object.new
|
|
89
|
+
|
|
90
|
+
# Add lazy secrets loading
|
|
91
|
+
temp_app.define_singleton_method(:secrets) do
|
|
92
|
+
@secrets ||= begin
|
|
93
|
+
# Convert to full paths and filter existing files
|
|
94
|
+
file_paths = files.map do |file|
|
|
95
|
+
file.is_a?(Pathname) ? file : Rails.root.join(file)
|
|
96
|
+
end.select(&:exist?)
|
|
97
|
+
|
|
98
|
+
if file_paths.any?
|
|
99
|
+
# Load secrets using Secvault
|
|
100
|
+
all_secrets = Secvault::Secrets.parse(file_paths, env: Rails.env)
|
|
101
|
+
current_secrets = ActiveSupport::OrderedOptions.new
|
|
102
|
+
current_secrets.merge!(all_secrets)
|
|
103
|
+
current_secrets
|
|
104
|
+
else
|
|
105
|
+
# Return empty secrets if no files found but include encryption structure
|
|
106
|
+
secrets = ActiveSupport::OrderedOptions.new
|
|
107
|
+
secrets.encryption = ActiveSupport::OrderedOptions.new
|
|
108
|
+
secrets.encryption.primary_key = nil
|
|
109
|
+
secrets.encryption.deterministic_key = nil
|
|
110
|
+
secrets.encryption.key_derivation_salt = nil
|
|
111
|
+
secrets
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
# Set up Rails.application to point to this temporary object
|
|
117
|
+
Rails.define_singleton_method(:application) { temp_app }
|
|
118
|
+
end
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
true
|
|
121
|
+
rescue => e
|
|
122
|
+
warn "[Secvault] Early application secrets setup failed: #{e.message}"
|
|
123
|
+
false
|
|
124
|
+
end
|
|
106
125
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
secrets_object
|
|
110
|
-
end
|
|
126
|
+
# Alias for backward compatibility
|
|
127
|
+
alias_method :setup_early_secrets!, :setup_early_application_secrets!
|
|
111
128
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Rails.application.config.secret_key_base = loaded_secrets["secret_key_base"]
|
|
115
|
-
unless Rails.env.production?
|
|
116
|
-
Rails.logger&.info "[Secvault] Set Rails.application.config.secret_key_base from secrets.yml"
|
|
117
|
-
end
|
|
118
|
-
end
|
|
129
|
+
def install!
|
|
130
|
+
return if defined?(Rails::Railtie).nil?
|
|
119
131
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
Rails.logger&.info "[Secvault] Integration complete. Loaded #{loaded_secrets.keys.size} secret keys."
|
|
123
|
-
end
|
|
124
|
-
else
|
|
125
|
-
Rails.logger&.warn "[Secvault] No secrets.yml file found at #{secrets_path}"
|
|
126
|
-
end
|
|
127
|
-
end
|
|
132
|
+
require "secvault/railtie"
|
|
133
|
+
require "secvault/rails_secrets"
|
|
128
134
|
end
|
|
129
135
|
|
|
130
|
-
#
|
|
131
|
-
#
|
|
136
|
+
# Start Secvault with simplified, unified API
|
|
137
|
+
# This is the main entry point for all Secvault functionality
|
|
132
138
|
#
|
|
133
|
-
# Usage
|
|
134
|
-
# Secvault.
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
+
# Usage examples:
|
|
140
|
+
# Secvault.start! # Simple: config/secrets.yml + Rails integration
|
|
141
|
+
# Secvault.start!(files: ['custom.yml']) # Custom single file
|
|
142
|
+
# Secvault.start!(files: ['base.yml', 'local.yml']) # Multiple files
|
|
143
|
+
# Secvault.start!(integrate_with_rails: false) # Load only, no Rails integration
|
|
144
|
+
# Secvault.start!(hot_reload: true) # Enable hot reload in development
|
|
145
|
+
#
|
|
146
|
+
# Access secrets:
|
|
147
|
+
# Rails.application.secrets.your_key # When integrate_rails: true (default)
|
|
148
|
+
# Secvault.secrets.your_key # Direct access (always available)
|
|
139
149
|
#
|
|
140
150
|
# Options:
|
|
141
|
-
# - files: Array of file paths (String or Pathname)
|
|
142
|
-
# -
|
|
143
|
-
# - logger: Enable/disable logging (default: true except in production)
|
|
144
|
-
# - suppress_warnings: Suppress Rails deprecation warnings about secrets (default: true)
|
|
151
|
+
# - files: Array of file paths (String or Pathname). Defaults to ['config/secrets.yml']
|
|
152
|
+
# - integrate_with_rails: Integrate with Rails.application.secrets (default: false)
|
|
145
153
|
# - set_secret_key_base: Set Rails.application.config.secret_key_base from secrets (default: true)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
#
|
|
152
|
-
|
|
154
|
+
# - hot_reload: Add reload_secrets! methods for development (default: true in development)
|
|
155
|
+
# - logger: Enable logging (default: true except production)
|
|
156
|
+
def start!(files: [], integrate_with_rails: false, set_secret_key_base: true,
|
|
157
|
+
hot_reload: ((defined?(Rails) && Rails.env.respond_to?(:development?)) ? Rails.env.development? : false),
|
|
158
|
+
logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
|
|
159
|
+
# Default to config/secrets.yml if no files specified
|
|
160
|
+
files_to_load = files.empty? ? ["config/secrets.yml"] : Array(files)
|
|
161
|
+
|
|
162
|
+
# Convert to Pathname objects and resolve relative to Rails.root
|
|
163
|
+
file_paths = files_to_load.map do |file|
|
|
153
164
|
file.is_a?(Pathname) ? file : Rails.root.join(file)
|
|
154
165
|
end
|
|
155
166
|
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
# Load secrets into Secvault.secrets
|
|
168
|
+
load_secrets!(file_paths, logger: logger)
|
|
169
|
+
|
|
170
|
+
# Integrate with Rails if requested
|
|
171
|
+
if integrate_with_rails
|
|
172
|
+
setup_rails_integration!(file_paths, set_secret_key_base: set_secret_key_base, logger: logger)
|
|
160
173
|
end
|
|
161
174
|
|
|
162
|
-
# Add reload
|
|
163
|
-
|
|
175
|
+
# Add hot reload functionality if requested
|
|
176
|
+
if hot_reload
|
|
177
|
+
add_hot_reload!(file_paths)
|
|
178
|
+
end
|
|
164
179
|
|
|
165
|
-
|
|
180
|
+
true
|
|
181
|
+
rescue => e
|
|
182
|
+
Rails.logger&.error "[Secvault] Failed to start: #{e.message}" if defined?(Rails) && logger
|
|
183
|
+
false
|
|
166
184
|
end
|
|
167
185
|
|
|
168
|
-
|
|
169
|
-
def load_secrets_only!(files, logger: !Rails.env.production?)
|
|
170
|
-
# Convert strings to Pathname objects and resolve relative to Rails.root
|
|
171
|
-
file_paths = Array(files).map do |file|
|
|
172
|
-
file.is_a?(Pathname) ? file : Rails.root.join(file)
|
|
173
|
-
end
|
|
186
|
+
private
|
|
174
187
|
|
|
188
|
+
# Load secrets into Secvault.secrets (internal storage)
|
|
189
|
+
def load_secrets!(file_paths, logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
|
|
175
190
|
existing_files = file_paths.select(&:exist?)
|
|
176
191
|
|
|
177
192
|
if existing_files.any?
|
|
178
|
-
# Load and merge all secrets files
|
|
193
|
+
# Load and merge all secrets files
|
|
179
194
|
merged_secrets = Secvault::Secrets.parse(existing_files, env: Rails.env)
|
|
180
195
|
|
|
181
|
-
# Store in
|
|
196
|
+
# Store in internal storage with ActiveSupport::OrderedOptions for compatibility
|
|
182
197
|
@@loaded_secrets = ActiveSupport::OrderedOptions.new
|
|
183
198
|
@@loaded_secrets.merge!(merged_secrets)
|
|
184
199
|
|
|
@@ -197,51 +212,51 @@ module Secvault
|
|
|
197
212
|
end
|
|
198
213
|
end
|
|
199
214
|
|
|
200
|
-
#
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if existing_files.any?
|
|
206
|
-
# Suppress Rails deprecation warnings about secrets if requested
|
|
207
|
-
suppress_secrets_deprecation_warning! if suppress_warnings
|
|
208
|
-
|
|
209
|
-
# Load and merge all secrets files
|
|
210
|
-
merged_secrets = Rails::Secrets.parse(existing_files, env: Rails.env)
|
|
211
|
-
|
|
212
|
-
# Create ActiveSupport::OrderedOptions object for Rails compatibility
|
|
213
|
-
secrets_object = ActiveSupport::OrderedOptions.new
|
|
214
|
-
secrets_object.merge!(merged_secrets)
|
|
215
|
+
# Set up Rails integration
|
|
216
|
+
def setup_rails_integration!(file_paths, set_secret_key_base: true, logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
|
|
217
|
+
# Override native Rails::Secrets with Secvault implementation
|
|
218
|
+
Rails.send(:remove_const, :Secrets) if defined?(Rails::Secrets)
|
|
219
|
+
Rails.const_set(:Secrets, Secvault::RailsSecrets)
|
|
215
220
|
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
# Set up Rails.application.secrets replacement in after_initialize
|
|
222
|
+
Rails.application.config.after_initialize do
|
|
223
|
+
if @@loaded_secrets && !@@loaded_secrets.empty?
|
|
224
|
+
# Replace Rails.application.secrets with our loaded secrets
|
|
225
|
+
Rails.application.define_singleton_method(:secrets) do
|
|
226
|
+
@@loaded_secrets
|
|
227
|
+
end
|
|
218
228
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
# Set secret_key_base in Rails config to avoid accessing it from secrets
|
|
230
|
+
if set_secret_key_base && @@loaded_secrets.key?(:secret_key_base)
|
|
231
|
+
Rails.application.config.secret_key_base = @@loaded_secrets[:secret_key_base]
|
|
232
|
+
Rails.logger&.info "[Secvault] Set Rails.application.config.secret_key_base from secrets" if logger
|
|
233
|
+
end
|
|
224
234
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
# Log integration success (except in production)
|
|
236
|
+
if logger
|
|
237
|
+
Rails.logger&.info "[Secvault] Rails integration complete. #{@@loaded_secrets.keys.size} secret keys available."
|
|
238
|
+
end
|
|
239
|
+
elsif logger
|
|
240
|
+
Rails.logger&.warn "[Secvault] No secrets loaded for Rails integration"
|
|
230
241
|
end
|
|
231
|
-
|
|
232
|
-
merged_secrets
|
|
233
|
-
else
|
|
234
|
-
Rails.logger&.warn "[Secvault Multi-File] No secrets files found" if logger
|
|
235
|
-
{}
|
|
236
242
|
end
|
|
237
243
|
end
|
|
238
244
|
|
|
239
|
-
# Add reload
|
|
240
|
-
def
|
|
245
|
+
# Add hot reload functionality for development
|
|
246
|
+
def add_hot_reload!(file_paths)
|
|
241
247
|
# Define reload method on Rails.application
|
|
242
248
|
Rails.application.define_singleton_method(:reload_secrets!) do
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
# Reload secrets
|
|
250
|
+
Secvault.send(:load_secrets!, file_paths, logger: true)
|
|
251
|
+
|
|
252
|
+
# Re-apply Rails integration if needed
|
|
253
|
+
if Secvault.rails_integrated? && @@loaded_secrets
|
|
254
|
+
Rails.application.define_singleton_method(:secrets) do
|
|
255
|
+
@@loaded_secrets
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
puts "🔄 Hot reloaded secrets from #{file_paths.size} files"
|
|
245
260
|
true
|
|
246
261
|
end
|
|
247
262
|
|
|
@@ -249,63 +264,63 @@ module Secvault
|
|
|
249
264
|
Object.define_method(:reload_secrets!) do
|
|
250
265
|
Rails.application.reload_secrets!
|
|
251
266
|
end
|
|
267
|
+
|
|
268
|
+
Rails.logger&.info "[Secvault] Hot reload enabled. Use reload_secrets! to refresh secrets." unless defined?(Rails) && Rails.env.respond_to?(:production?) && Rails.env.production?
|
|
252
269
|
end
|
|
253
270
|
|
|
254
|
-
|
|
255
|
-
#
|
|
256
|
-
# Usage:
|
|
257
|
-
# Secvault.start! # Uses config/secrets.yml only
|
|
258
|
-
# Secvault.start!(files: []) # Same as above
|
|
259
|
-
# Secvault.start!(files: ['path/to/secrets.yml']) # Custom single file
|
|
260
|
-
# Secvault.start!(files: ['gem.yml', 'app.yml']) # Multiple files
|
|
261
|
-
#
|
|
262
|
-
# Access loaded secrets via: Secvault.secrets.your_key
|
|
263
|
-
# To integrate with Rails.application.secrets, call: Secvault.integrate_with_rails!
|
|
264
|
-
#
|
|
265
|
-
# Options:
|
|
266
|
-
# - files: Array of file paths (String or Pathname). Defaults to ['config/secrets.yml']
|
|
267
|
-
# - logger: Enable logging (default: true except production)
|
|
268
|
-
def start!(files: [], logger: !Rails.env.production?)
|
|
269
|
-
# Default to host app's config/secrets.yml if no files specified
|
|
270
|
-
files_to_load = files.empty? ? ["config/secrets.yml"] : files
|
|
271
|
+
public
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
load_secrets_only!(files_to_load, logger: logger)
|
|
273
|
+
end
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
# Auto-install and setup when Rails is available
|
|
276
|
+
if defined?(Rails)
|
|
277
|
+
Secvault.install!
|
|
278
|
+
|
|
279
|
+
# Immediate setup for early access during application loading
|
|
280
|
+
begin
|
|
281
|
+
# Try to detect and load secrets immediately if Rails.root is available
|
|
282
|
+
if Rails.respond_to?(:root) && Rails.root
|
|
283
|
+
# Look for default secrets or configuration
|
|
284
|
+
default_secrets_file = Rails.root.join("config/secrets.yml")
|
|
285
|
+
commons_secrets_file = nil
|
|
286
|
+
|
|
287
|
+
# Check for neeto-commons-backend integration
|
|
288
|
+
if defined?(NeetoCommonsBackend) && NeetoCommonsBackend.respond_to?(:shared_secrets_file)
|
|
289
|
+
commons_secrets_file = NeetoCommonsBackend.shared_secrets_file
|
|
290
|
+
end
|
|
280
291
|
|
|
281
|
-
|
|
282
|
-
def integrate_with_rails!
|
|
283
|
-
return false unless @@loaded_secrets
|
|
292
|
+
files_to_load = [commons_secrets_file, default_secrets_file].compact.select(&:exist?)
|
|
284
293
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
Rails.send(:remove_const, :Secrets) if defined?(Rails::Secrets)
|
|
289
|
-
Rails.const_set(:Secrets, Secvault::RailsSecrets)
|
|
290
|
-
end
|
|
294
|
+
if files_to_load.any? && Rails.respond_to?(:env)
|
|
295
|
+
# Load secrets immediately
|
|
296
|
+
all_secrets = Secvault::Secrets.parse(files_to_load, env: Rails.env)
|
|
291
297
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
298
|
+
# Set up Rails.application.secrets if Rails.application exists
|
|
299
|
+
if Rails.respond_to?(:application) && Rails.application
|
|
300
|
+
Rails.application.define_singleton_method(:secrets) do
|
|
301
|
+
@secrets ||= begin
|
|
302
|
+
current_secrets = ActiveSupport::OrderedOptions.new
|
|
303
|
+
current_secrets.merge!(all_secrets)
|
|
304
|
+
current_secrets
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
else
|
|
308
|
+
# Create a minimal Rails.application for early access
|
|
309
|
+
temp_app = Object.new
|
|
310
|
+
temp_app.define_singleton_method(:secrets) do
|
|
311
|
+
@secrets ||= begin
|
|
312
|
+
current_secrets = ActiveSupport::OrderedOptions.new
|
|
313
|
+
current_secrets.merge!(all_secrets)
|
|
314
|
+
current_secrets
|
|
315
|
+
end
|
|
316
|
+
end
|
|
296
317
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
Rails.logger&.error "[Secvault] Failed to integrate with Rails: #{e.message}" if defined?(Rails)
|
|
301
|
-
false
|
|
318
|
+
Rails.define_singleton_method(:application) { temp_app } unless Rails.respond_to?(:application)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
302
321
|
end
|
|
322
|
+
rescue => e
|
|
323
|
+
# Silent fail - normal initialization will handle it
|
|
324
|
+
warn "[Secvault] Early auto-load failed: #{e.message}" unless Rails.env&.production?
|
|
303
325
|
end
|
|
304
|
-
|
|
305
|
-
# Backward compatibility aliases
|
|
306
|
-
alias_method :setup_backward_compatibility_with_older_rails!, :setup! # Legacy name
|
|
307
|
-
alias_method :setup_rails_71_integration!, :setup! # Legacy name
|
|
308
|
-
alias_method :setup_multi_files!, :setup_multi_file! # Alternative name
|
|
309
326
|
end
|
|
310
|
-
|
|
311
|
-
Secvault.install! if defined?(Rails)
|