vaultez-cli 0.3.2 → 0.4.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/vaultez/cli.rb +29 -1
- data/lib/vaultez/commands/auth.rb +4 -4
- data/lib/vaultez/commands/config_command.rb +4 -4
- data/lib/vaultez/commands/fetch.rb +40 -39
- data/lib/vaultez/output.rb +88 -0
- data/lib/vaultez/version.rb +1 -1
- data/lib/vaultez.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ec4178a4e79116eef6ff1b13b8db47f134cb9c57ed0fa85e727871886acb2fa
|
|
4
|
+
data.tar.gz: 23ee0ad5bf641a03a7a8cc3763ab1b2c140c50a07927dac808eded42d2e7a421
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4e00d6f19821fffe29bfac9430b125a9dbab05f8e3a2d53f73bdae8e4c6adb820999ef32d563d3825601978df128d31ef827b33a07aa267d21f1e4c182f7e00
|
|
7
|
+
data.tar.gz: e53532d610033a006a3f04e8153d35e081b1605fd55ed7f5de4b813a0cf67f4f3191b0206844cd068a0e14c0912eed9d4e241aa1e942e21149d77342767a4873
|
data/lib/vaultez/cli.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "thor"
|
|
2
2
|
require_relative "version"
|
|
3
|
+
require_relative "output"
|
|
3
4
|
require_relative "commands/auth"
|
|
4
5
|
require_relative "commands/fetch"
|
|
5
6
|
require_relative "commands/config_command"
|
|
@@ -12,6 +13,12 @@ module Vaultez
|
|
|
12
13
|
|
|
13
14
|
map %w[--version -v] => :__version
|
|
14
15
|
|
|
16
|
+
# Usage errors (unknown flag, bad --format) must exit non-zero, or
|
|
17
|
+
# `vaultez fetch ... > .env && ./start.sh` carries on as if it worked.
|
|
18
|
+
def self.exit_on_failure?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
15
22
|
desc "login", "Authenticate with email, password, and 2FA code"
|
|
16
23
|
long_desc <<~DESC, wrap: false
|
|
17
24
|
Authenticate with your Vaultez account. You will be prompted for your
|
|
@@ -54,6 +61,26 @@ module Vaultez
|
|
|
54
61
|
VAULTEZ_TOKEN=vz_... vaultez fetch
|
|
55
62
|
VAULTEZ_TOKEN=vz_... vaultez fetch --secret="DATABASE_URL"
|
|
56
63
|
|
|
64
|
+
OUTPUT FORMATS (--format, for a project's secrets):
|
|
65
|
+
env KEY='value' lines, quoted so they are safe to eval or source (default)
|
|
66
|
+
eval "$(vaultez fetch --project="Backend")"
|
|
67
|
+
set -a; source <(vaultez fetch --project="Backend"); set +a
|
|
68
|
+
shell export KEY='value' lines
|
|
69
|
+
dotenv a .env file for dotenv, Next.js, Vite or Docker Compose
|
|
70
|
+
vaultez fetch --project="Backend" --format=dotenv > .env.local
|
|
71
|
+
github entries for $GITHUB_ENV, multi-line values included
|
|
72
|
+
vaultez fetch --format=github >> "$GITHUB_ENV"
|
|
73
|
+
json a JSON array of {id, name, value} (same as --json)
|
|
74
|
+
|
|
75
|
+
Secrets whose names aren't valid variable names (letters, numbers and _,
|
|
76
|
+
not starting with a number) are skipped, with a warning on stderr. They
|
|
77
|
+
are still included in json output.
|
|
78
|
+
|
|
79
|
+
--secret prints just the value, unquoted, or a JSON object with --format=json.
|
|
80
|
+
|
|
81
|
+
Stdout only ever carries data. Errors and notices such as "No secrets
|
|
82
|
+
found" go to stderr, and errors exit with status 1.
|
|
83
|
+
|
|
57
84
|
Project tokens can be created in the Tokens tab of your project settings.
|
|
58
85
|
Always pass the token via the VAULTEZ_TOKEN environment variable, never
|
|
59
86
|
as a command-line flag — CLI arguments are visible to other local users
|
|
@@ -64,7 +91,8 @@ module Vaultez
|
|
|
64
91
|
option :projects, type: :boolean, desc: "List projects in a company"
|
|
65
92
|
option :project, type: :string, desc: "Project name"
|
|
66
93
|
option :secret, type: :string, desc: "Secret name (returns value only)"
|
|
67
|
-
option :
|
|
94
|
+
option :format, type: :string, enum: Vaultez::Output::FORMATS, desc: "Output format (default: env)"
|
|
95
|
+
option :json, type: :boolean, desc: "Output as JSON (same as --format=json)"
|
|
68
96
|
def fetch; super; end
|
|
69
97
|
|
|
70
98
|
desc "config", "Set default company or token"
|
|
@@ -20,11 +20,11 @@ module Vaultez
|
|
|
20
20
|
Vaultez::Config.set("token", response["token"])
|
|
21
21
|
puts "Logged in successfully."
|
|
22
22
|
rescue Vaultez::TwoFactorRequiredError => error
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
warn "Error: #{error.message}"
|
|
24
|
+
warn "Set up two-factor authentication at https://vaultez.app/two_factor/new"
|
|
25
25
|
exit 1
|
|
26
26
|
rescue Vaultez::AuthenticationError => error
|
|
27
|
-
|
|
27
|
+
warn "Error: #{error.message}"
|
|
28
28
|
exit 1
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -34,7 +34,7 @@ module Vaultez
|
|
|
34
34
|
Vaultez::Config.clear
|
|
35
35
|
puts "Logged out successfully."
|
|
36
36
|
rescue Vaultez::NotAuthenticatedError => error
|
|
37
|
-
|
|
37
|
+
warn "Error: #{error.message}"
|
|
38
38
|
exit 1
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -8,21 +8,21 @@ module Vaultez
|
|
|
8
8
|
company = companies.find { |company| company["name"] == options[:"default-company"] }
|
|
9
9
|
|
|
10
10
|
unless company
|
|
11
|
-
|
|
11
|
+
warn "Error: company \"#{options[:"default-company"]}\" not found."
|
|
12
12
|
exit 1
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
Vaultez::Config.set("default_company", company["name"])
|
|
16
16
|
puts "Default company set to \"#{company["name"]}\"."
|
|
17
17
|
else
|
|
18
|
-
|
|
18
|
+
warn "No config option provided. See `vaultez help config`."
|
|
19
19
|
exit 1
|
|
20
20
|
end
|
|
21
21
|
rescue Vaultez::NotAuthenticatedError => error
|
|
22
|
-
|
|
22
|
+
warn "Error: #{error.message}"
|
|
23
23
|
exit 1
|
|
24
24
|
rescue Vaultez::ApiError => error
|
|
25
|
-
|
|
25
|
+
warn "Error: #{error.message}"
|
|
26
26
|
exit 1
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require_relative "../output"
|
|
2
3
|
|
|
3
4
|
module Vaultez
|
|
4
5
|
module Commands
|
|
@@ -29,13 +30,40 @@ module Vaultez
|
|
|
29
30
|
|
|
30
31
|
private
|
|
31
32
|
|
|
33
|
+
# Stdout carries only data, so `vaultez fetch > .env` never writes an
|
|
34
|
+
# error or a notice into the file. Everything else goes to stderr.
|
|
32
35
|
def fail!(message)
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
warn "Error: #{message}"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def output_format
|
|
41
|
+
@output_format ||= begin
|
|
42
|
+
format = options[:format]
|
|
43
|
+
if options[:json] && format && format != "json"
|
|
44
|
+
fail!("--json can't be combined with --format=#{format}.")
|
|
45
|
+
end
|
|
46
|
+
format || (options[:json] ? "json" : Vaultez::Output::DEFAULT_FORMAT)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def json?
|
|
51
|
+
output_format == "json"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def print_secret_value(secret)
|
|
55
|
+
if json?
|
|
56
|
+
puts secret.to_json
|
|
35
57
|
else
|
|
36
|
-
|
|
58
|
+
print secret["value"]
|
|
37
59
|
end
|
|
38
|
-
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def print_secrets(secrets, empty_message)
|
|
63
|
+
warn empty_message if secrets.empty?
|
|
64
|
+
text, warnings = Vaultez::Output.render(secrets, output_format)
|
|
65
|
+
print text
|
|
66
|
+
warnings.each { |message| warn "Warning: #{message}" }
|
|
39
67
|
end
|
|
40
68
|
|
|
41
69
|
def fetch_with_project_token(client)
|
|
@@ -45,22 +73,9 @@ module Vaultez
|
|
|
45
73
|
unless secret
|
|
46
74
|
fail!("secret \"#{options[:secret]}\" not found.")
|
|
47
75
|
end
|
|
48
|
-
|
|
49
|
-
puts secret.to_json
|
|
50
|
-
else
|
|
51
|
-
print secret["value"]
|
|
52
|
-
end
|
|
76
|
+
print_secret_value(secret)
|
|
53
77
|
else
|
|
54
|
-
|
|
55
|
-
if options[:json]
|
|
56
|
-
puts secrets.to_json
|
|
57
|
-
return
|
|
58
|
-
end
|
|
59
|
-
if secrets.empty?
|
|
60
|
-
puts "No secrets found."
|
|
61
|
-
return
|
|
62
|
-
end
|
|
63
|
-
secrets.each { |s| puts "#{s["name"]}=#{s["value"]}" }
|
|
78
|
+
print_secrets(fetch_all_secrets_for_token(client), "No secrets found.")
|
|
64
79
|
end
|
|
65
80
|
end
|
|
66
81
|
|
|
@@ -80,12 +95,12 @@ module Vaultez
|
|
|
80
95
|
|
|
81
96
|
def fetch_companies(client)
|
|
82
97
|
companies = client.companies
|
|
83
|
-
if
|
|
98
|
+
if json?
|
|
84
99
|
puts companies.to_json
|
|
85
100
|
return
|
|
86
101
|
end
|
|
87
102
|
if companies.empty?
|
|
88
|
-
|
|
103
|
+
warn "No companies found."
|
|
89
104
|
return
|
|
90
105
|
end
|
|
91
106
|
puts "Companies:"
|
|
@@ -97,12 +112,12 @@ module Vaultez
|
|
|
97
112
|
def fetch_projects(client)
|
|
98
113
|
company = resolve_company(client)
|
|
99
114
|
projects = client.projects(company["id"])
|
|
100
|
-
if
|
|
115
|
+
if json?
|
|
101
116
|
puts projects.to_json
|
|
102
117
|
return
|
|
103
118
|
end
|
|
104
119
|
if projects.empty?
|
|
105
|
-
|
|
120
|
+
warn "No projects found in #{company["name"]}."
|
|
106
121
|
return
|
|
107
122
|
end
|
|
108
123
|
puts "Projects in #{company["name"]}:"
|
|
@@ -115,17 +130,7 @@ module Vaultez
|
|
|
115
130
|
company = resolve_company(client)
|
|
116
131
|
project = resolve_project(client, company)
|
|
117
132
|
secrets = client.secrets(project["id"])
|
|
118
|
-
|
|
119
|
-
puts secrets.to_json
|
|
120
|
-
return
|
|
121
|
-
end
|
|
122
|
-
if secrets.empty?
|
|
123
|
-
puts "No secrets found in #{project["name"]}."
|
|
124
|
-
return
|
|
125
|
-
end
|
|
126
|
-
secrets.each do |secret|
|
|
127
|
-
puts "#{secret["name"]}=#{secret["value"]}"
|
|
128
|
-
end
|
|
133
|
+
print_secrets(secrets, "No secrets found in #{project["name"]}.")
|
|
129
134
|
end
|
|
130
135
|
|
|
131
136
|
def fetch_secret(client)
|
|
@@ -138,11 +143,7 @@ module Vaultez
|
|
|
138
143
|
fail!("secret \"#{options[:secret]}\" not found in #{project["name"]}.")
|
|
139
144
|
end
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
puts secret.to_json
|
|
143
|
-
else
|
|
144
|
-
print secret["value"]
|
|
145
|
-
end
|
|
146
|
+
print_secret_value(secret)
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
def resolve_company(client)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
|
|
4
|
+
module Vaultez
|
|
5
|
+
# Renders a project's secrets for the formats `vaultez fetch --format=...`
|
|
6
|
+
# accepts. Every format except json must be safe to hand to its consumer
|
|
7
|
+
# as-is: a secret value is data written by any project editor, so it must
|
|
8
|
+
# never be able to run a command, end its own line early, or set a second
|
|
9
|
+
# variable on the machine of whoever sources the output.
|
|
10
|
+
module Output
|
|
11
|
+
FORMATS = %w[env shell dotenv github json].freeze
|
|
12
|
+
DEFAULT_FORMAT = "env".freeze
|
|
13
|
+
|
|
14
|
+
VARIABLE_NAME = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def valid_name?(name)
|
|
19
|
+
name.is_a?(String) && VARIABLE_NAME.match?(name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns [text, warnings]; the caller prints the warnings on stderr.
|
|
23
|
+
# Secrets whose names can't be variable names are left out of the
|
|
24
|
+
# non-json formats, since no shell or dotenv parser would accept the line.
|
|
25
|
+
def render(secrets, format)
|
|
26
|
+
return [JSON.generate(secrets) + "\n", []] if format == "json"
|
|
27
|
+
|
|
28
|
+
warnings = []
|
|
29
|
+
lines = secrets.filter_map do |secret|
|
|
30
|
+
name, value = secret["name"], secret["value"].to_s
|
|
31
|
+
unless valid_name?(name)
|
|
32
|
+
warnings << "skipped secret #{name.inspect}: not a valid variable name. " \
|
|
33
|
+
"Rename it to use only letters, numbers and _ (not starting with a number), or use --format=json."
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
if format == "dotenv" && !portable_in_dotenv?(value)
|
|
37
|
+
warnings << "secret #{name.inspect} contains ' together with \", \\ or $. Ruby dotenv and " \
|
|
38
|
+
"Docker Compose read it correctly; Node's dotenv keeps the backslash escapes."
|
|
39
|
+
end
|
|
40
|
+
line(name, value, format)
|
|
41
|
+
end
|
|
42
|
+
[lines.join, warnings]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def line(name, value, format)
|
|
46
|
+
case format
|
|
47
|
+
when "env" then "#{name}=#{shell_quote(value)}\n"
|
|
48
|
+
when "shell" then "export #{name}=#{shell_quote(value)}\n"
|
|
49
|
+
when "dotenv" then "#{name}=#{dotenv_quote(value)}\n"
|
|
50
|
+
when "github" then github_entry(name, value)
|
|
51
|
+
else raise ArgumentError, "unknown format: #{format}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# POSIX single quotes: nothing inside is special except the closing
|
|
56
|
+
# quote itself, which is written as '\'' (close, escaped quote, reopen).
|
|
57
|
+
def shell_quote(value)
|
|
58
|
+
"'#{value.gsub("'", %q('\\\\''))}'"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Single-quoted values, line breaks included, are literal in every
|
|
62
|
+
# common dotenv parser (Ruby dotenv, Node dotenv, Docker Compose), so use
|
|
63
|
+
# them whenever the value has no ' of its own. Double quotes are next:
|
|
64
|
+
# also literal as long as there's no ", \ or $ to interpret. Only a value
|
|
65
|
+
# with both needs escapes, and parsers disagree on those: Ruby dotenv and
|
|
66
|
+
# Compose unescape \" \\ \$ (and Ruby runs $(...) if $ isn't escaped),
|
|
67
|
+
# while Node's dotenv leaves the backslashes in.
|
|
68
|
+
def dotenv_quote(value)
|
|
69
|
+
return "'#{value}'" unless value.include?("'")
|
|
70
|
+
return "\"#{value}\"" unless value.match?(/["\\$]/)
|
|
71
|
+
|
|
72
|
+
"\"#{value.gsub(/["\\$]/) { |char| "\\#{char}" }}\""
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def portable_in_dotenv?(value)
|
|
76
|
+
!(value.include?("'") && value.match?(/["\\$]/))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# GitHub's multiline syntax for $GITHUB_ENV. A random delimiter means a
|
|
80
|
+
# value can't close the block early and smuggle in another variable
|
|
81
|
+
# (e.g. NODE_OPTIONS) for later workflow steps.
|
|
82
|
+
def github_entry(name, value)
|
|
83
|
+
delimiter = "VAULTEZ_EOF_#{SecureRandom.hex(16)}"
|
|
84
|
+
delimiter = "VAULTEZ_EOF_#{SecureRandom.hex(16)}" while value.include?(delimiter)
|
|
85
|
+
"#{name}<<#{delimiter}\n#{value}\n#{delimiter}\n"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/vaultez/version.rb
CHANGED
data/lib/vaultez.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vaultez-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nur Ketene
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- lib/vaultez/commands/fetch.rb
|
|
40
40
|
- lib/vaultez/config.rb
|
|
41
41
|
- lib/vaultez/errors.rb
|
|
42
|
+
- lib/vaultez/output.rb
|
|
42
43
|
- lib/vaultez/version.rb
|
|
43
44
|
homepage: https://vaultez.app
|
|
44
45
|
licenses:
|
|
@@ -58,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
59
|
- !ruby/object:Gem::Version
|
|
59
60
|
version: '0'
|
|
60
61
|
requirements: []
|
|
61
|
-
rubygems_version: 4.0.
|
|
62
|
+
rubygems_version: 4.0.20
|
|
62
63
|
specification_version: 4
|
|
63
64
|
summary: CLI tool for Vaultez — manage your secrets from the terminal
|
|
64
65
|
test_files: []
|