vaultez-cli 0.1.0 → 0.2.1

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: '07770618a022bbabf6a89511dbeb1366ead244d90fc938435aba94bb9e7ccbbd'
4
- data.tar.gz: 03afca8c6b3846442c78791cddda0232b9882d37104708b31ce8e38e4e985a11
3
+ metadata.gz: 5691471c3bf1c3a9c144c9dc7a6a3acb621e786a3ff6c8dda909a37cb840f9cd
4
+ data.tar.gz: 33fa1a5e4031ec260923526bdb9cc4bc7b99f301fe163cc71407963d92ace921
5
5
  SHA512:
6
- metadata.gz: 5346e840aa94d43ea44fb8c6a0394bc511e1d139bc812f2fcf95c4616619e35a210aa8b94b2d7616b7dfaa271a13bb8759674ae5b752c8d0239fa6742d9a50d4
7
- data.tar.gz: 2dda8d7e5fb79f72d3b8b5b65dc338a5af379149b28cd949984a4ce24ad6db86b5d7b9d138eec86f331278d83246da738c5aa0235fcfa723b13c7f32db6a6614
6
+ metadata.gz: a2a50894e4fbcb228b1d65579d93689ca22740574b917502a1d91664a7cf63def38e047362a06fb30b323be78e95f3e154de1c9ba1d7b03c1ac6ae6aeadb75bb
7
+ data.tar.gz: 6f59a21a570e06772789c18b52772420427a81c58840e0db1fa7cb6750bc040c1f8eb9366ca536bd4807b831d5d5e5b8bc0d4895465f33838bae53da12d1c43f
data/lib/vaultez/cli.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "thor"
2
+ require_relative "version"
2
3
  require_relative "commands/auth"
3
4
  require_relative "commands/fetch"
4
5
  require_relative "commands/config_command"
@@ -9,17 +10,30 @@ module Vaultez
9
10
  include Vaultez::Commands::Fetch
10
11
  include Vaultez::Commands::ConfigCommand
11
12
 
12
- desc "login", "Authenticate with Vaultez"
13
+ map %w[--version -v] => :__version
14
+
15
+ class_option :token, type: :string, banner: "TOKEN",
16
+ desc: "Use a project token instead of the saved session"
17
+
18
+ def initialize(*args)
19
+ super
20
+ ENV["VAULTEZ_TOKEN"] = options[:token] if options[:token]
21
+ end
22
+
23
+ desc "login", "Authenticate with email, password, and 2FA code"
13
24
  long_desc <<~DESC, wrap: false
14
25
  Authenticate with your Vaultez account. You will be prompted for your
15
- email and password. Your session token is stored in ~/.vaultez/config.yml.
26
+ email, password, and a two-factor authentication code from your authenticator
27
+ app (or a backup code). Your session token is stored in ~/.vaultez/config.yml.
28
+
29
+ Two-factor authentication is required for all accounts.
16
30
 
17
31
  Example:
18
32
  vaultez login
19
33
  DESC
20
34
  def login; super; end
21
35
 
22
- desc "logout", "Log out and revoke your token"
36
+ desc "logout", "Revoke the current session token"
23
37
  long_desc <<~DESC, wrap: false
24
38
  Revokes your session token on the server and clears your local credentials.
25
39
 
@@ -28,25 +42,37 @@ module Vaultez
28
42
  DESC
29
43
  def logout; super; end
30
44
 
31
- desc "fetch", "Fetch companies, projects, or secrets"
45
+ desc "fetch", "Fetch secrets from a project"
32
46
  long_desc <<~DESC, wrap: false
33
- Fetch resources from Vaultez. The --company flag is optional when you
34
- have a default company set or only belong to one company.
47
+ Fetch resources from Vaultez.
48
+
49
+ USER SESSION (after `vaultez login`):
50
+ The --company flag is optional when you have a default company set or
51
+ only belong to one company. Use --project to scope to a project.
35
52
 
36
- Examples:
37
53
  vaultez fetch --companies
38
54
  vaultez fetch --company="Acme" --projects
39
55
  vaultez fetch --company="Acme" --project="Backend"
40
56
  vaultez fetch --company="Acme" --project="Backend" --secret="DATABASE_URL"
57
+
58
+ PROJECT TOKEN (VAULTEZ_TOKEN env var or --token flag):
59
+ When a project token is active, it already knows which project to use.
60
+ No --project flag is needed.
61
+
62
+ VAULTEZ_TOKEN=vz_... vaultez fetch
63
+ vaultez fetch --token=vz_... --secret="DATABASE_URL"
64
+
65
+ Project tokens can be created in the Tokens tab of your project settings.
41
66
  DESC
42
67
  option :companies, type: :boolean, desc: "List all your companies"
43
68
  option :company, type: :string, desc: "Company name"
44
69
  option :projects, type: :boolean, desc: "List projects in a company"
45
70
  option :project, type: :string, desc: "Project name"
46
- option :secret, type: :string, desc: "Secret name"
71
+ option :secret, type: :string, desc: "Secret name (returns value only)"
72
+ option :json, type: :boolean, desc: "Output as JSON (errors go to stderr)"
47
73
  def fetch; super; end
48
74
 
49
- desc "config", "Configure Vaultez CLI settings"
75
+ desc "config", "Set default company or token"
50
76
  long_desc <<~DESC, wrap: false
51
77
  Update your local Vaultez CLI configuration.
52
78
 
@@ -55,5 +81,38 @@ module Vaultez
55
81
  DESC
56
82
  option :"default-company", type: :string, desc: "Set the default company"
57
83
  def config; super; end
84
+
85
+ desc "__version", "Show the installed CLI version", hide: true
86
+ def __version
87
+ puts "vaultez-cli #{Vaultez::VERSION}"
88
+ end
89
+
90
+ def help(command = nil, subcommand: false)
91
+ if command
92
+ super
93
+ else
94
+ puts "Vaultez CLI — secure secret management from your terminal"
95
+ puts
96
+ puts "Usage: vaultez <command> [options]"
97
+ puts
98
+ puts "Commands:"
99
+ puts " login Authenticate with email, password, and 2FA code"
100
+ puts " logout Revoke the current session token"
101
+ puts " fetch Fetch secrets from a project"
102
+ puts " config Set default company or token"
103
+ puts " help Show help for any command"
104
+ puts
105
+ puts "Options:"
106
+ puts " --token Use a project token instead of the saved session"
107
+ puts " --version Show the installed CLI version"
108
+ puts
109
+ puts "Examples:"
110
+ puts " vaultez login"
111
+ puts " vaultez fetch --project=\"Backend\""
112
+ puts " vaultez fetch --project=\"Backend\" --secret=\"DATABASE_URL\""
113
+ puts " vaultez fetch --token=\"vz_...\""
114
+ puts " vaultez fetch --token=\"vz_...\" --secret=\"DATABASE_URL\""
115
+ end
116
+ end
58
117
  end
59
118
  end
@@ -5,18 +5,23 @@ require "uri"
5
5
  module Vaultez
6
6
  class Client
7
7
  def initialize
8
- @api_url = Vaultez::Config.api_url
9
- @token = Vaultez::Config.token
8
+ @api_url = Vaultez::Config.api_url
9
+ @token = Vaultez::Config.token
10
+ @project_token = ENV["VAULTEZ_TOKEN"]
10
11
  end
11
12
 
12
- def login(email, password)
13
- post("/api/v1/auth/login", { email: email, password: password }, authenticated: false)
13
+ def login(email, password, otp_code)
14
+ post("/api/v1/auth/login", { email: email, password: password, otp_code: otp_code }, authenticated: false)
14
15
  end
15
16
 
16
17
  def logout
17
18
  delete("/api/v1/auth/logout")
18
19
  end
19
20
 
21
+ def project_token_mode?
22
+ !@project_token.nil?
23
+ end
24
+
20
25
  def companies
21
26
  get("/api/v1/companies")
22
27
  end
@@ -53,8 +58,9 @@ module Vaultez
53
58
  req["Accept"] = "application/json"
54
59
 
55
60
  if authenticated
56
- raise Vaultez::NotAuthenticatedError, "Not logged in. Run `vaultez login` first." unless @token
57
- req["Authorization"] = "Bearer #{@token}"
61
+ active_token = @project_token || @token
62
+ raise Vaultez::NotAuthenticatedError, "Not logged in. Run `vaultez login` first, or set VAULTEZ_TOKEN." unless active_token
63
+ req["Authorization"] = "Bearer #{active_token}"
58
64
  end
59
65
 
60
66
  req.body = body.to_json if body
@@ -75,9 +81,10 @@ module Vaultez
75
81
 
76
82
  case response.code.to_i
77
83
  when 200, 201 then body
78
- when 401 then raise Vaultez::AuthenticationError, body["error"] || "Authentication failed"
79
- when 404 then raise Vaultez::NotFoundError, body["error"] || "Not found"
80
- else raise Vaultez::ApiError, body["error"] || "API error (#{response.code})"
84
+ when 401 then raise Vaultez::AuthenticationError, body["error"] || "Authentication failed"
85
+ when 403 then raise Vaultez::TwoFactorRequiredError, body["error"] || "Two-factor authentication required"
86
+ when 404 then raise Vaultez::NotFoundError, body["error"] || "Not found"
87
+ else raise Vaultez::ApiError, body["error"] || "API error (#{response.code})"
81
88
  end
82
89
  end
83
90
  end
@@ -11,11 +11,18 @@ module Vaultez
11
11
  system("stty echo")
12
12
  puts
13
13
 
14
+ puts "One-time code (Proton Authenticator / TOTP): "
15
+ otp_code = $stdin.gets.chomp
16
+
14
17
  client = Vaultez::Client.new
15
- response = client.login(email, password)
18
+ response = client.login(email, password, otp_code)
16
19
 
17
20
  Vaultez::Config.set("token", response["token"])
18
21
  puts "Logged in successfully."
22
+ rescue Vaultez::TwoFactorRequiredError => error
23
+ puts "Error: #{error.message}"
24
+ puts "Set up two-factor authentication at https://vaultez.app/two_factor/new"
25
+ exit 1
19
26
  rescue Vaultez::AuthenticationError => error
20
27
  puts "Error: #{error.message}"
21
28
  exit 1
@@ -1,10 +1,14 @@
1
+ require "json"
2
+
1
3
  module Vaultez
2
4
  module Commands
3
5
  module Fetch
4
6
  def fetch
5
7
  client = Vaultez::Client.new
6
8
 
7
- if options[:companies]
9
+ if client.project_token_mode?
10
+ fetch_with_project_token(client)
11
+ elsif options[:companies]
8
12
  fetch_companies(client)
9
13
  elsif options[:projects]
10
14
  fetch_projects(client)
@@ -13,24 +17,73 @@ module Vaultez
13
17
  elsif options[:project]
14
18
  fetch_secrets(client)
15
19
  else
16
- puts "Error: not enough options. See `vaultez help fetch`."
17
- exit 1
20
+ fail!("not enough options. See `vaultez help fetch`.")
18
21
  end
19
22
  rescue Vaultez::NotAuthenticatedError => error
20
- puts "Error: #{error.message}"
21
- exit 1
23
+ fail!(error.message)
22
24
  rescue Vaultez::NotFoundError => error
23
- puts "Error: #{error.message}"
24
- exit 1
25
+ fail!(error.message)
25
26
  rescue Vaultez::ApiError => error
26
- puts "Error: #{error.message}"
27
- exit 1
27
+ fail!(error.message)
28
28
  end
29
29
 
30
30
  private
31
31
 
32
+ def fail!(message)
33
+ if options[:json]
34
+ warn "Error: #{message}"
35
+ else
36
+ puts "Error: #{message}"
37
+ end
38
+ exit 1
39
+ end
40
+
41
+ def fetch_with_project_token(client)
42
+ if options[:secret]
43
+ secrets = fetch_all_secrets_for_token(client)
44
+ secret = secrets.find { |s| s["name"] == options[:secret] }
45
+ unless secret
46
+ fail!("secret \"#{options[:secret]}\" not found.")
47
+ end
48
+ if options[:json]
49
+ puts secret.to_json
50
+ else
51
+ print secret["value"]
52
+ end
53
+ else
54
+ secrets = fetch_all_secrets_for_token(client)
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"]}" }
64
+ end
65
+ end
66
+
67
+ def fetch_all_secrets_for_token(client)
68
+ companies = client.companies
69
+ company = companies.first
70
+ unless company
71
+ fail!("no company found for this token.")
72
+ end
73
+ projects = client.projects(company["id"])
74
+ project = projects.first
75
+ unless project
76
+ fail!("no project found for this token.")
77
+ end
78
+ client.secrets(project["id"])
79
+ end
80
+
32
81
  def fetch_companies(client)
33
82
  companies = client.companies
83
+ if options[:json]
84
+ puts companies.to_json
85
+ return
86
+ end
34
87
  if companies.empty?
35
88
  puts "No companies found."
36
89
  return
@@ -44,6 +97,10 @@ module Vaultez
44
97
  def fetch_projects(client)
45
98
  company = resolve_company(client)
46
99
  projects = client.projects(company["id"])
100
+ if options[:json]
101
+ puts projects.to_json
102
+ return
103
+ end
47
104
  if projects.empty?
48
105
  puts "No projects found in #{company["name"]}."
49
106
  return
@@ -58,6 +115,10 @@ module Vaultez
58
115
  company = resolve_company(client)
59
116
  project = resolve_project(client, company)
60
117
  secrets = client.secrets(project["id"])
118
+ if options[:json]
119
+ puts secrets.to_json
120
+ return
121
+ end
61
122
  if secrets.empty?
62
123
  puts "No secrets found in #{project["name"]}."
63
124
  return
@@ -71,48 +132,46 @@ module Vaultez
71
132
  company = resolve_company(client)
72
133
  project = resolve_project(client, company)
73
134
  secrets = client.secrets(project["id"])
74
- secret = secrets.find { |secret| secret["name"] == options[:secret] }
135
+ secret = secrets.find { |s| s["name"] == options[:secret] }
75
136
 
76
137
  unless secret
77
- puts "Error: secret \"#{options[:secret]}\" not found in #{project["name"]}."
78
- exit 1
138
+ fail!("secret \"#{options[:secret]}\" not found in #{project["name"]}.")
79
139
  end
80
140
 
81
- print secret["value"]
141
+ if options[:json]
142
+ puts secret.to_json
143
+ else
144
+ print secret["value"]
145
+ end
82
146
  end
83
147
 
84
148
  def resolve_company(client)
85
149
  companies = client.companies
86
150
 
87
151
  if options[:company]
88
- company = companies.find { |company| company["name"] == options[:company] }
152
+ company = companies.find { |c| c["name"] == options[:company] }
89
153
  unless company
90
- puts "Error: company \"#{options[:company]}\" not found."
91
- exit 1
154
+ fail!("company \"#{options[:company]}\" not found.")
92
155
  end
93
156
  return company
94
157
  end
95
158
 
96
159
  default_name = Vaultez::Config.default_company
97
160
  if default_name
98
- company = companies.find { |company| company["name"] == default_name }
161
+ company = companies.find { |c| c["name"] == default_name }
99
162
  return company if company
100
163
  end
101
164
 
102
- if companies.size == 1
103
- return companies.first
104
- end
165
+ return companies.first if companies.size == 1
105
166
 
106
- puts "Error: multiple companies found. Specify one with --company or set a default with `vaultez config --default-company`."
107
- exit 1
167
+ fail!("multiple companies found. Specify one with --company or set a default with `vaultez config --default-company`.")
108
168
  end
109
169
 
110
170
  def resolve_project(client, company)
111
171
  projects = client.projects(company["id"])
112
- project = projects.find { |project| project["name"] == options[:project] }
172
+ project = projects.find { |p| p["name"] == options[:project] }
113
173
  unless project
114
- puts "Error: project \"#{options[:project]}\" not found in #{company["name"]}."
115
- exit 1
174
+ fail!("project \"#{options[:project]}\" not found in #{company["name"]}.")
116
175
  end
117
176
  project
118
177
  end
@@ -26,7 +26,7 @@ module Vaultez
26
26
  end
27
27
 
28
28
  def self.token
29
- get("token")
29
+ ENV["VAULTEZ_TOKEN"] || get("token")
30
30
  end
31
31
 
32
32
  def self.api_url
@@ -1,7 +1,8 @@
1
1
  module Vaultez
2
- class Error < StandardError; end
3
- class NotAuthenticatedError < Error; end
4
- class AuthenticationError < Error; end
5
- class NotFoundError < Error; end
6
- class ApiError < Error; end
2
+ class Error < StandardError; end
3
+ class NotAuthenticatedError < Error; end
4
+ class AuthenticationError < Error; end
5
+ class TwoFactorRequiredError < Error; end
6
+ class NotFoundError < Error; end
7
+ class ApiError < Error; end
7
8
  end
@@ -1,3 +1,3 @@
1
1
  module Vaultez
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
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.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nur Ketene