localvault 1.12.3 → 1.14.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/lib/localvault/cli/rails_import.rb +58 -0
- data/lib/localvault/cli.rb +150 -2
- data/lib/localvault/env_projection.rb +15 -0
- data/lib/localvault/input_validation.rb +1 -1
- data/lib/localvault/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f77c11508eef73c90760e5a927201f6735a16d836e6890161e89a497e28d917
|
|
4
|
+
data.tar.gz: 15400be786d2ba53112d1b6b48a1de5bd0518903e5578c859677ec68320f2b8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8fcd7235f9e330cdf7cf2c17528849f7b64b4388e865351ab53c1758b3390d32dd638c2c21e7ba24ff18e7d9c63e7c5e0bd2c66efa87792380d9d8ef12b61b7
|
|
7
|
+
data.tar.gz: c8491e0b0fcb2e588080496254b4fa04195859ad81d7ce66c38d4e13116d20374fa9f96668cc18cf8dc2a0830affb58ea59df84ed6eb1531c817942f364c54e3
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module LocalVault
|
|
2
|
+
class CLI
|
|
3
|
+
# Finds a Rails app's credentials keys so they can be stored in the vault
|
|
4
|
+
# and injected as RAILS_MASTER_KEY instead of living in a file.
|
|
5
|
+
#
|
|
6
|
+
# Reads only. Removing the key files is the operator's call — this reports
|
|
7
|
+
# what is still on disk and leaves it alone.
|
|
8
|
+
class RailsImport
|
|
9
|
+
Key = Struct.new(:environment, :path, :vault_key, :value, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
MASTER_KEY_PATH = "config/master.key".freeze
|
|
12
|
+
CREDENTIALS_PATH = "config/credentials.yml.enc".freeze
|
|
13
|
+
|
|
14
|
+
def initialize(root = Dir.pwd)
|
|
15
|
+
@root = root
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rails_app?
|
|
19
|
+
File.exist?(File.join(@root, CREDENTIALS_PATH)) ||
|
|
20
|
+
File.exist?(File.join(@root, MASTER_KEY_PATH)) ||
|
|
21
|
+
Dir.exist?(File.join(@root, "config/credentials"))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Every key file this app has: the default master key plus one per
|
|
25
|
+
# environment (config/credentials/production.key).
|
|
26
|
+
def keys
|
|
27
|
+
[master_key, *environment_keys].compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def master_key
|
|
33
|
+
path = File.join(@root, MASTER_KEY_PATH)
|
|
34
|
+
return nil unless File.exist?(path)
|
|
35
|
+
|
|
36
|
+
value = File.read(path).strip
|
|
37
|
+
return nil if value.empty?
|
|
38
|
+
|
|
39
|
+
Key.new(environment: nil, path: MASTER_KEY_PATH, vault_key: "rails.master_key", value: value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def environment_keys
|
|
43
|
+
Dir.glob(File.join(@root, "config/credentials/*.key")).sort.filter_map do |path|
|
|
44
|
+
environment = File.basename(path, ".key")
|
|
45
|
+
value = File.read(path).strip
|
|
46
|
+
next if value.empty?
|
|
47
|
+
|
|
48
|
+
Key.new(
|
|
49
|
+
environment: environment,
|
|
50
|
+
path: "config/credentials/#{environment}.key",
|
|
51
|
+
vault_key: "rails.#{environment}_key",
|
|
52
|
+
value: value
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/localvault/cli.rb
CHANGED
|
@@ -116,14 +116,14 @@ module LocalVault
|
|
|
116
116
|
# disappearing from help.
|
|
117
117
|
HELP_SECTIONS = [
|
|
118
118
|
["GETTING STARTED", %w[login config init demo]],
|
|
119
|
-
["SECRETS", %w[set get show reveal groups list delete import env exec]],
|
|
119
|
+
["SECRETS", %w[set get show reveal groups list delete import env exec rails]],
|
|
120
120
|
["VAULT MANAGEMENT", %w[vaults switch rekey unlock lock reset rename copy]],
|
|
121
121
|
["SYNC (requires localvault login)", %w[sync]],
|
|
122
122
|
["TEAM SHARING (requires localvault login)", %w[dashboard verify add remove team]],
|
|
123
123
|
["KEYS (X25519 identity for vault sharing)", %w[keys identity]],
|
|
124
124
|
["AI / MCP", %w[install-mcp mcp guard]],
|
|
125
125
|
["LEGACY SHARING (pre-v1.2 direct share, still works)", %w[keygen share receive revoke]],
|
|
126
|
-
["OTHER", %w[logout version doctor help]]
|
|
126
|
+
["OTHER", %w[logout version doctor upgrade help]]
|
|
127
127
|
].freeze
|
|
128
128
|
|
|
129
129
|
# One row per plain command; registered subcommands (sync, team, keys,
|
|
@@ -807,6 +807,7 @@ module LocalVault
|
|
|
807
807
|
require_relative "cli/sync"
|
|
808
808
|
require_relative "cli/guard"
|
|
809
809
|
require_relative "cli/identity_cmd"
|
|
810
|
+
require_relative "cli/rails_import"
|
|
810
811
|
|
|
811
812
|
# Thor 1.5 injects a `tree` command into every class. Inside a namespace it
|
|
812
813
|
# lists under a name that isn't even callable (`identity_command tree`), and
|
|
@@ -1649,6 +1650,121 @@ module LocalVault
|
|
|
1649
1650
|
end
|
|
1650
1651
|
end
|
|
1651
1652
|
|
|
1653
|
+
desc "rails", "Import a Rails app's credentials keys into the vault"
|
|
1654
|
+
long_desc <<~DESC
|
|
1655
|
+
Store this Rails app's credentials keys in the vault so you can run it
|
|
1656
|
+
with no key file on disk. Rails reads ENV["RAILS_MASTER_KEY"] whenever
|
|
1657
|
+
config/master.key is absent, so injecting that one variable is enough.
|
|
1658
|
+
|
|
1659
|
+
\x05 localvault rails # import config/master.key (and per-env keys)
|
|
1660
|
+
\x05 localvault rails --check # show what would be imported
|
|
1661
|
+
|
|
1662
|
+
Then run your app with the key injected, never written anywhere:
|
|
1663
|
+
|
|
1664
|
+
\x05 localvault exec --profile rails -- bin/rails server
|
|
1665
|
+
\x05 localvault exec --profile rails -- bin/rails credentials:edit
|
|
1666
|
+
|
|
1667
|
+
For a per-environment key (config/credentials/production.key), map it —
|
|
1668
|
+
Rails reads no variable other than RAILS_MASTER_KEY:
|
|
1669
|
+
|
|
1670
|
+
\x05 localvault exec --map rails.production_key=RAILS_MASTER_KEY -- bin/rails console
|
|
1671
|
+
|
|
1672
|
+
Key files are left exactly as they are. Deleting them is your call.
|
|
1673
|
+
DESC
|
|
1674
|
+
method_option :check, type: :boolean, default: false, desc: "List what would be imported without writing"
|
|
1675
|
+
def rails
|
|
1676
|
+
importer = RailsImport.new
|
|
1677
|
+
unless importer.rails_app?
|
|
1678
|
+
abort_with "No Rails app here — expected config/credentials.yml.enc or config/master.key in #{Dir.pwd}"
|
|
1679
|
+
return CommandStatus.error
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
keys = importer.keys
|
|
1683
|
+
if keys.empty?
|
|
1684
|
+
$stdout.puts "No credentials key files found (config/master.key, config/credentials/*.key)."
|
|
1685
|
+
$stdout.puts "Nothing to import — the app may already run from RAILS_MASTER_KEY."
|
|
1686
|
+
return CommandStatus.ok
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
if options[:check]
|
|
1690
|
+
$stdout.puts "Would import into vault '#{options[:vault] || Config.default_vault}':"
|
|
1691
|
+
keys.each { |key| $stdout.puts " #{key.path} -> #{key.vault_key}" }
|
|
1692
|
+
return CommandStatus.ok
|
|
1693
|
+
end
|
|
1694
|
+
|
|
1695
|
+
vault = open_vault!
|
|
1696
|
+
keys.each do |key|
|
|
1697
|
+
vault.set(key.vault_key, key.value)
|
|
1698
|
+
$stdout.puts "Imported #{key.path} -> #{key.vault_key}"
|
|
1699
|
+
end
|
|
1700
|
+
|
|
1701
|
+
$stdout.puts
|
|
1702
|
+
$stdout.puts "Run without a key file on disk:"
|
|
1703
|
+
$stdout.puts " localvault exec --profile rails -- bin/rails server"
|
|
1704
|
+
keys.select(&:environment).each do |key|
|
|
1705
|
+
$stdout.puts " localvault exec --map #{key.vault_key}=RAILS_MASTER_KEY -- bin/rails console # #{key.environment}"
|
|
1706
|
+
end
|
|
1707
|
+
|
|
1708
|
+
$stdout.puts
|
|
1709
|
+
$stdout.puts "Note: the key files are still on disk (#{keys.map(&:path).join(", ")})."
|
|
1710
|
+
$stdout.puts "They keep working until you remove them — that's your call, not ours."
|
|
1711
|
+
CommandStatus.ok
|
|
1712
|
+
end
|
|
1713
|
+
|
|
1714
|
+
desc "upgrade", "Upgrade localvault using whichever method installed it"
|
|
1715
|
+
long_desc <<~DESC
|
|
1716
|
+
Detect how this copy of localvault was installed and run the matching
|
|
1717
|
+
upgrade — Homebrew, RubyGems, or the install script.
|
|
1718
|
+
|
|
1719
|
+
\x05 localvault upgrade # detect and upgrade
|
|
1720
|
+
\x05 localvault upgrade --check # show what would run, change nothing
|
|
1721
|
+
|
|
1722
|
+
If more than one localvault is on PATH, an old copy can shadow the new one
|
|
1723
|
+
and the upgrade looks like it did nothing. This refuses to guess in that
|
|
1724
|
+
case and points you at `localvault doctor`.
|
|
1725
|
+
DESC
|
|
1726
|
+
method_option :check, type: :boolean, default: false, desc: "Print the upgrade command without running it"
|
|
1727
|
+
def upgrade
|
|
1728
|
+
paths = localvault_paths
|
|
1729
|
+
if paths.length > 1
|
|
1730
|
+
$stderr.puts "Error: multiple localvault executables are on PATH:"
|
|
1731
|
+
paths.each_with_index { |path, index| $stderr.puts " #{index + 1}. #{path}" }
|
|
1732
|
+
$stderr.puts "\nUpgrading now would leave a stale copy shadowing the new one."
|
|
1733
|
+
$stderr.puts "Run `localvault doctor`, remove the copies you don't want, then retry."
|
|
1734
|
+
return CommandStatus.error
|
|
1735
|
+
end
|
|
1736
|
+
|
|
1737
|
+
method, command = upgrade_method_for(paths.first)
|
|
1738
|
+
if method == :unknown
|
|
1739
|
+
$stderr.puts "Error: cannot tell how localvault was installed#{" (#{paths.first})" if paths.first}."
|
|
1740
|
+
$stderr.puts "Upgrade with whichever you used:"
|
|
1741
|
+
$stderr.puts " brew upgrade inventlist/tap/localvault"
|
|
1742
|
+
$stderr.puts " gem update localvault"
|
|
1743
|
+
$stderr.puts " curl -sSL https://inventlist.com/tools/localvault/install.sh | sh"
|
|
1744
|
+
return CommandStatus.error
|
|
1745
|
+
end
|
|
1746
|
+
|
|
1747
|
+
$stdout.puts "Installed via #{method} (#{paths.first})"
|
|
1748
|
+
$stdout.puts "Current version: #{VERSION}"
|
|
1749
|
+
$stdout.puts "Command: #{command}"
|
|
1750
|
+
|
|
1751
|
+
if options[:check]
|
|
1752
|
+
$stdout.puts "\n--check: nothing was run."
|
|
1753
|
+
return CommandStatus.ok
|
|
1754
|
+
end
|
|
1755
|
+
|
|
1756
|
+
$stdout.puts
|
|
1757
|
+
unless system(command)
|
|
1758
|
+
$stderr.puts "\nError: upgrade command failed. Run it yourself to see the full output:"
|
|
1759
|
+
$stderr.puts " #{command}"
|
|
1760
|
+
return CommandStatus.error
|
|
1761
|
+
end
|
|
1762
|
+
|
|
1763
|
+
$stdout.puts
|
|
1764
|
+
$stdout.puts "Done. Verify with: localvault version"
|
|
1765
|
+
CommandStatus.ok
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1652
1768
|
def self.exit_on_failure?
|
|
1653
1769
|
false
|
|
1654
1770
|
end
|
|
@@ -2169,6 +2285,38 @@ module LocalVault
|
|
|
2169
2285
|
path.start_with?("/opt/homebrew/bin/", "/usr/local/bin/")
|
|
2170
2286
|
end
|
|
2171
2287
|
|
|
2288
|
+
# Which installer put this executable here, and how to upgrade it.
|
|
2289
|
+
#
|
|
2290
|
+
# The path alone can't decide: Homebrew and install.sh both land in
|
|
2291
|
+
# /usr/local/bin. Both write a wrapper naming their gem prefix, so read
|
|
2292
|
+
# that instead — a Cellar path means brew, ~/.localvault/runtime means the
|
|
2293
|
+
# install script.
|
|
2294
|
+
UPGRADE_COMMANDS = {
|
|
2295
|
+
brew: "brew upgrade inventlist/tap/localvault",
|
|
2296
|
+
gem: "gem update localvault",
|
|
2297
|
+
script: "curl -sSL https://inventlist.com/tools/localvault/install.sh | sh"
|
|
2298
|
+
}.freeze
|
|
2299
|
+
|
|
2300
|
+
def upgrade_method_for(path)
|
|
2301
|
+
method = detect_install_method(path)
|
|
2302
|
+
[method, UPGRADE_COMMANDS[method]]
|
|
2303
|
+
end
|
|
2304
|
+
|
|
2305
|
+
def detect_install_method(path)
|
|
2306
|
+
return :unknown if path.nil?
|
|
2307
|
+
return :brew if File.symlink?(path) && File.readlink(path).include?("/Cellar/")
|
|
2308
|
+
|
|
2309
|
+
wrapper = File.read(path, 2048)
|
|
2310
|
+
return :brew if wrapper.include?("/Cellar/")
|
|
2311
|
+
return :script if wrapper.include?("/.localvault/runtime")
|
|
2312
|
+
return :gem if path.include?("/shims/") || path.include?("/gems/")
|
|
2313
|
+
|
|
2314
|
+
:unknown
|
|
2315
|
+
rescue SystemCallError, ArgumentError
|
|
2316
|
+
# Binary or unreadable: fall back to the one thing the path does tell us.
|
|
2317
|
+
path.include?("/shims/") || path.include?("/gems/") ? :gem : :unknown
|
|
2318
|
+
end
|
|
2319
|
+
|
|
2172
2320
|
def cursor_settings_path
|
|
2173
2321
|
File.expand_path("~/.cursor/mcp.json")
|
|
2174
2322
|
end
|
|
@@ -17,9 +17,24 @@ module LocalVault
|
|
|
17
17
|
"AWS_IAM.secret_access_key" => "AWS_SECRET_ACCESS_KEY",
|
|
18
18
|
"AWS_IAM.session_token" => "AWS_SESSION_TOKEN"
|
|
19
19
|
}
|
|
20
|
+
},
|
|
21
|
+
# Rails reads its credentials key from ENV["RAILS_MASTER_KEY"] when
|
|
22
|
+
# config/master.key is absent (railties: encrypted(..., env_key:
|
|
23
|
+
# "RAILS_MASTER_KEY")), so injecting that one variable is enough to run a
|
|
24
|
+
# Rails app with no key file on disk.
|
|
25
|
+
"rails" => {
|
|
26
|
+
only: ["rails.*"],
|
|
27
|
+
map: { "rails.master_key" => "RAILS_MASTER_KEY" }
|
|
20
28
|
}
|
|
21
29
|
}.freeze
|
|
22
30
|
|
|
31
|
+
# Per-environment credentials (config/credentials/production.key) still have
|
|
32
|
+
# to arrive as RAILS_MASTER_KEY — Rails reads no other variable. This builds
|
|
33
|
+
# the mapping for one environment.
|
|
34
|
+
def self.rails_environment_mapping(environment)
|
|
35
|
+
{ "rails.#{environment}_key" => "RAILS_MASTER_KEY" }
|
|
36
|
+
end
|
|
37
|
+
|
|
23
38
|
def self.entries(secrets, project: nil, only: nil, except: nil, map: nil, profile: nil, on_skip: nil)
|
|
24
39
|
profile_config = profile_config(profile)
|
|
25
40
|
selectors = parse_selectors(only) || profile_config[:only]
|
data/lib/localvault/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: localvault
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nauman Tariq
|
|
@@ -115,6 +115,7 @@ files:
|
|
|
115
115
|
- lib/localvault/cli/help_shell.rb
|
|
116
116
|
- lib/localvault/cli/identity_cmd.rb
|
|
117
117
|
- lib/localvault/cli/keys.rb
|
|
118
|
+
- lib/localvault/cli/rails_import.rb
|
|
118
119
|
- lib/localvault/cli/sync.rb
|
|
119
120
|
- lib/localvault/cli/team.rb
|
|
120
121
|
- lib/localvault/cli/team_helpers.rb
|