2pass 1.2.0 → 2.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/2pass.gemspec +2 -0
- data/README.md +12 -5
- data/bin/2pass +40 -23
- data/lib/2pass/version.rb +1 -1
- data/lib/2pass.rb +140 -29
- metadata +17 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 135196a393e121fb8b71d5295fdedc19f3bccb24885ce81cf630be59d2b7c64e
|
|
4
|
+
data.tar.gz: 7f5684e81780732115fcf6b9d5a071cfd1e8a05eaa48246e35f7fd3841a702ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1c997e62173b57c596adf21c9636bfd7ed3417a5f0611d497db1caa486a18e113ad37b234bec8b3334fcea282c5f99f2ec9dbd1b4e328222b1439f281629586
|
|
7
|
+
data.tar.gz: 9697b86fe839dc5d29f7786ab2e278986fbdb64bd2fc89e142f6e8bc812f919a66d3b57afcb97e71e099d806cc1411c56c12f5117facf6174ee780de71812b14
|
data/2pass.gemspec
CHANGED
|
@@ -24,4 +24,6 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
25
25
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
26
26
|
spec.add_development_dependency "minitest-reporters", "~> 1.3", ">= 1.3.0"
|
|
27
|
+
|
|
28
|
+
spec.add_dependency "terminal-table", "~> 4.0"
|
|
27
29
|
end
|
data/README.md
CHANGED
|
@@ -17,13 +17,12 @@ An array of hashes with the following keys:
|
|
|
17
17
|
|
|
18
18
|
- id
|
|
19
19
|
- value
|
|
20
|
-
- uuid
|
|
21
20
|
|
|
22
21
|
```sh
|
|
23
22
|
touch ~/.2pass/vault_name.yml
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
Alternatively, if you have a vault file in a different location, you can link it.
|
|
25
|
+
Alternatively, if you already have a vault file in a different location, you can link it.
|
|
27
26
|
|
|
28
27
|
```sh
|
|
29
28
|
2pass link vault_name /path/to/vault_name.yml
|
|
@@ -40,10 +39,18 @@ Then build the gem and install it.
|
|
|
40
39
|
|
|
41
40
|
```sh
|
|
42
41
|
2pass -h
|
|
43
|
-
2pass get <vault_name> <id>
|
|
44
|
-
2pass list <vault_name>
|
|
45
|
-
2pass
|
|
42
|
+
2pass get <vault_name> <id> [env]
|
|
43
|
+
2pass list <vault_name> [env]
|
|
44
|
+
2pass list <vault_name> [env] --json
|
|
45
|
+
2pass list <vault_name> [env] --dotenv
|
|
46
|
+
2pass add <vault_name> <id> <value> [env]
|
|
47
|
+
2pass link <vault_name> <target_path> [env]
|
|
48
|
+
2pass update
|
|
46
49
|
```
|
|
50
|
+
You can also pass an optional environment to most commands:
|
|
51
|
+
`2pass get myproject mykey production`
|
|
52
|
+
|
|
53
|
+
This will search for `mykey` within `myproject.production.yml`
|
|
47
54
|
|
|
48
55
|
## Development
|
|
49
56
|
|
data/bin/2pass
CHANGED
|
@@ -5,12 +5,14 @@ require_relative "../lib/2pass"
|
|
|
5
5
|
def help_message
|
|
6
6
|
puts <<~HELP
|
|
7
7
|
Usage:
|
|
8
|
-
2pass add <vault_name> <id> <value>
|
|
9
|
-
2pass get <vault_name> <id>
|
|
10
|
-
2pass list <vault_name>
|
|
11
|
-
2pass list <vault_name> --json
|
|
12
|
-
2pass
|
|
13
|
-
2pass -
|
|
8
|
+
2pass add <vault_name> <id> <value> [env] - Add new secret. env is optional
|
|
9
|
+
2pass get <vault_name> <id> [env] - Get content by ID from the specified vault. env is optional
|
|
10
|
+
2pass list <vault_name> [env] - List the content of the specified vault as key=value pairs. env is optional
|
|
11
|
+
2pass list <vault_name> [env] --json - List the content of the specified vault as JSON. env is optional
|
|
12
|
+
2pass list <vault_name> [env] --dotenv - List the content of the specified vault as key=value pairs. env is optional
|
|
13
|
+
2pass link <vault_name> <target_path> [env] - Create a symlink for an existing vault. Useful when the vault is stored in a synced place (iCloud, Dropbox, etc.). env is optional
|
|
14
|
+
2pass update - Update 2pass to the latest RubyGems release
|
|
15
|
+
2pass -h - Display this help message
|
|
14
16
|
HELP
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -24,12 +26,12 @@ OptionParser.new do |opts|
|
|
|
24
26
|
opts.on("-v", "--version", "Display the version") do
|
|
25
27
|
options[:version] = true
|
|
26
28
|
end
|
|
27
|
-
opts.on("
|
|
28
|
-
options[:format] = format == "json" ? :json : :text
|
|
29
|
-
end
|
|
30
|
-
opts.on("--json", "Use JSON output format (same as -f json)") do
|
|
29
|
+
opts.on("--json", "Use JSON output format") do
|
|
31
30
|
options[:format] = :json
|
|
32
31
|
end
|
|
32
|
+
opts.on("--dotenv", "Use key=value output format") do
|
|
33
|
+
options[:format] = :dotenv
|
|
34
|
+
end
|
|
33
35
|
end.parse!
|
|
34
36
|
|
|
35
37
|
if options[:help]
|
|
@@ -42,46 +44,61 @@ if options[:version]
|
|
|
42
44
|
exit
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
if ARGV.
|
|
47
|
+
if ARGV.empty?
|
|
46
48
|
help_message
|
|
47
49
|
exit(1)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
|
-
command,
|
|
52
|
+
command, *args = ARGV
|
|
51
53
|
|
|
52
54
|
begin
|
|
53
55
|
case command&.to_sym
|
|
54
56
|
when :get
|
|
55
|
-
if args.length <
|
|
57
|
+
if args.length < 2 || args.length > 3
|
|
56
58
|
help_message
|
|
57
59
|
exit(1)
|
|
58
60
|
end
|
|
59
|
-
id = args[0]
|
|
60
|
-
puts TwoPass.get_secret(vault_name, id)
|
|
61
|
+
vault_name, id, env = args[0], args[1], args[2]
|
|
62
|
+
puts TwoPass.get_secret(vault_name, id, env: env)
|
|
61
63
|
when :add
|
|
62
|
-
if args.length
|
|
64
|
+
if args.length < 3 || args.length > 4
|
|
63
65
|
help_message
|
|
64
66
|
exit(1)
|
|
65
67
|
end
|
|
66
|
-
|
|
68
|
+
vault_name, id, value, env = args[0], args[1], args[2], args[3]
|
|
69
|
+
TwoPass.add_secret(vault_name, id, value, env: env)
|
|
67
70
|
when :list
|
|
71
|
+
if args.length < 1 || args.length > 2
|
|
72
|
+
help_message
|
|
73
|
+
exit(1)
|
|
74
|
+
end
|
|
75
|
+
vault_name, env = args[0], args[1]
|
|
68
76
|
if options[:format] == :json
|
|
69
|
-
puts TwoPass.list_content(vault_name, format: :json)
|
|
77
|
+
puts TwoPass.list_content(vault_name, env: env, format: :json)
|
|
78
|
+
elsif options[:format] == :dotenv
|
|
79
|
+
puts TwoPass.list_content(vault_name, env: env, format: :dotenv)
|
|
70
80
|
else
|
|
71
|
-
puts TwoPass.list_content(vault_name)
|
|
81
|
+
puts TwoPass.list_content(vault_name, env: env)
|
|
72
82
|
end
|
|
73
|
-
exit(
|
|
83
|
+
exit(0)
|
|
74
84
|
when :link
|
|
75
|
-
if args.length <
|
|
85
|
+
if args.length < 2 || args.length > 3
|
|
76
86
|
help_message
|
|
77
87
|
exit(1)
|
|
78
88
|
end
|
|
79
|
-
target_path = args[0]
|
|
80
|
-
TwoPass.create_symlink(vault_name, target_path)
|
|
89
|
+
vault_name, target_path, env = args[0], args[1], args[2]
|
|
90
|
+
TwoPass.create_symlink(vault_name, target_path, env: env)
|
|
91
|
+
when :update
|
|
92
|
+
if args.any?
|
|
93
|
+
help_message
|
|
94
|
+
exit(1)
|
|
95
|
+
end
|
|
96
|
+
puts TwoPass.update_cli
|
|
81
97
|
when :help
|
|
82
98
|
help_message
|
|
83
99
|
else
|
|
84
100
|
help_message
|
|
101
|
+
exit(1)
|
|
85
102
|
end
|
|
86
103
|
rescue => e
|
|
87
104
|
STDERR.puts e.message
|
data/lib/2pass/version.rb
CHANGED
data/lib/2pass.rb
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "optparse"
|
|
2
4
|
require "json"
|
|
3
5
|
require "yaml"
|
|
4
6
|
require "fileutils"
|
|
5
|
-
require "
|
|
7
|
+
require "net/http"
|
|
8
|
+
require "uri"
|
|
9
|
+
require "rubygems/version"
|
|
10
|
+
require "terminal-table"
|
|
6
11
|
|
|
7
12
|
require_relative "2pass/version"
|
|
8
13
|
|
|
@@ -10,42 +15,62 @@ module TwoPass
|
|
|
10
15
|
VAULT_DIR = "#{Dir.home}/.2pass"
|
|
11
16
|
|
|
12
17
|
class << self
|
|
13
|
-
def create_symlink(vault_name, target_path)
|
|
18
|
+
def create_symlink(vault_name, target_path, env: nil)
|
|
14
19
|
FileUtils.mkdir_p(VAULT_DIR)
|
|
15
|
-
symlink_path =
|
|
20
|
+
symlink_path = vault_file_path(vault_name, env: env)
|
|
21
|
+
expanded_target_path = File.expand_path(target_path)
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
else
|
|
20
|
-
File.symlink(target_path, symlink_path)
|
|
21
|
-
puts "Created symlink: #{symlink_path} -> #{target_path}"
|
|
23
|
+
unless File.exist?(expanded_target_path)
|
|
24
|
+
raise "Target vault file does not exist: #{expanded_target_path}"
|
|
22
25
|
end
|
|
23
|
-
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
unless File.readable?(expanded_target_path)
|
|
28
|
+
raise "Target vault file is not readable: #{expanded_target_path}"
|
|
29
|
+
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if File.symlink?(symlink_path)
|
|
32
|
+
existing_target = File.expand_path(File.readlink(symlink_path), File.dirname(symlink_path))
|
|
33
|
+
if existing_target == expanded_target_path
|
|
34
|
+
puts "Symlink already exists: #{symlink_path} -> #{expanded_target_path}"
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
raise "A different symlink already exists at #{symlink_path}: #{existing_target}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if File.exist?(symlink_path)
|
|
42
|
+
raise "A regular file already exists at #{symlink_path}. Remove it before creating a symlink."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
File.symlink(expanded_target_path, symlink_path)
|
|
46
|
+
puts "Created symlink: #{symlink_path} -> #{expanded_target_path}"
|
|
33
47
|
end
|
|
34
48
|
|
|
35
|
-
def save_vault(vault_name, data)
|
|
49
|
+
def save_vault(vault_name, data, env: nil)
|
|
36
50
|
FileUtils.mkdir_p(VAULT_DIR)
|
|
37
|
-
file_path =
|
|
51
|
+
file_path = vault_file_path(vault_name, env: env)
|
|
38
52
|
File.write(file_path, YAML.dump(data))
|
|
39
53
|
end
|
|
40
54
|
|
|
41
|
-
def list_content(vault_name, format: :
|
|
42
|
-
vault = load_vault(vault_name)
|
|
43
|
-
if format == :
|
|
55
|
+
def list_content(vault_name, env: nil, format: :table)
|
|
56
|
+
vault = load_vault(vault_name, env: env)
|
|
57
|
+
if format == :table
|
|
58
|
+
rows = vault
|
|
59
|
+
.sort_by { |hash| hash[:id] }
|
|
60
|
+
.each_with_object([]) do |h, arr|
|
|
61
|
+
arr << [h[:id], h[:value]]
|
|
62
|
+
end
|
|
63
|
+
Terminal::Table.new(
|
|
64
|
+
rows: rows,
|
|
65
|
+
title: env || nil,
|
|
66
|
+
style: {all_separators: true}
|
|
67
|
+
).to_s
|
|
68
|
+
elsif format == :dotenv
|
|
44
69
|
vault
|
|
45
70
|
.map { |h| "#{h[:id]}=#{h[:value]}" }
|
|
46
71
|
.sort
|
|
47
72
|
.join("\n")
|
|
48
|
-
|
|
73
|
+
elsif format == :json
|
|
49
74
|
JSON.pretty_generate(
|
|
50
75
|
vault
|
|
51
76
|
.map { |hash| hash.slice(:id, :value) }
|
|
@@ -54,23 +79,22 @@ module TwoPass
|
|
|
54
79
|
end
|
|
55
80
|
end
|
|
56
81
|
|
|
57
|
-
def add_secret(vault_name, id, value)
|
|
82
|
+
def add_secret(vault_name, id, value, env: nil)
|
|
58
83
|
new_secret = {
|
|
59
84
|
id: id,
|
|
60
|
-
value: value
|
|
61
|
-
uuid: SecureRandom.uuid
|
|
85
|
+
value: value
|
|
62
86
|
}
|
|
63
|
-
data = load_vault(vault_name)
|
|
87
|
+
data = load_vault(vault_name, env: env)
|
|
64
88
|
existing_entry_id = data.find_index { |hash| hash[:id] == new_secret[:id] }
|
|
65
89
|
if existing_entry_id
|
|
66
90
|
raise "The secret already exists"
|
|
67
91
|
end
|
|
68
92
|
data << new_secret
|
|
69
|
-
save_vault(vault_name, data)
|
|
93
|
+
save_vault(vault_name, data, env: env)
|
|
70
94
|
end
|
|
71
95
|
|
|
72
|
-
def get_secret(vault_name, id)
|
|
73
|
-
vault = load_vault(vault_name)
|
|
96
|
+
def get_secret(vault_name, id, env: nil)
|
|
97
|
+
vault = load_vault(vault_name, env: env)
|
|
74
98
|
entry = vault.find { |hash| hash[:id] == id }
|
|
75
99
|
if entry
|
|
76
100
|
entry[:value]
|
|
@@ -78,5 +102,92 @@ module TwoPass
|
|
|
78
102
|
raise "Entry not found"
|
|
79
103
|
end
|
|
80
104
|
end
|
|
105
|
+
|
|
106
|
+
def update_cli
|
|
107
|
+
latest_version = fetch_latest_version
|
|
108
|
+
current = Gem::Version.new(VERSION)
|
|
109
|
+
latest = Gem::Version.new(latest_version)
|
|
110
|
+
|
|
111
|
+
if current >= latest
|
|
112
|
+
return "2pass is already up to date (#{VERSION})."
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
unless run_update_command
|
|
116
|
+
raise "Failed to update 2pass. Try running: #{update_command.join(' ')}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
"Updated 2pass from #{VERSION} to #{latest_version}. Restart your shell session if needed."
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def load_vault(vault_name, env: nil)
|
|
125
|
+
file_path = self.vault_file_path(vault_name, env: env)
|
|
126
|
+
return [] unless File.exist?(file_path)
|
|
127
|
+
|
|
128
|
+
YAML.load_file(file_path, symbolize_names: true) || []
|
|
129
|
+
rescue Errno::EACCES, Errno::EPERM => e
|
|
130
|
+
raise permission_error_message(file_path, e)
|
|
131
|
+
rescue Psych::SyntaxError => e
|
|
132
|
+
STDERR.puts "Error parsing YAML file: #{e.message}"
|
|
133
|
+
[]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def vault_file_path(vault_name, env: nil)
|
|
137
|
+
[
|
|
138
|
+
VAULT_DIR,
|
|
139
|
+
[vault_name, env].compact.join(".")
|
|
140
|
+
].join("/") + ".yml"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def permission_error_message(file_path, error)
|
|
144
|
+
message = +"Unable to read vault file: #{file_path}\n#{error.class}: #{error.message}"
|
|
145
|
+
symlink_target = nil
|
|
146
|
+
|
|
147
|
+
if File.symlink?(file_path)
|
|
148
|
+
symlink_target = File.expand_path(File.readlink(file_path), File.dirname(file_path))
|
|
149
|
+
message << "\nSymlink target: #{symlink_target}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
path_to_check = symlink_target || file_path
|
|
153
|
+
if path_to_check.include?("/Library/Mobile Documents/")
|
|
154
|
+
message << "\nThe vault points to iCloud Drive. macOS may be blocking your terminal app."
|
|
155
|
+
message << "\nCheck System Settings > Privacy & Security > Files and Folders"
|
|
156
|
+
message << "\nfor your terminal app and enable iCloud Drive (and Full Disk Access if needed)."
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
message
|
|
160
|
+
rescue StandardError
|
|
161
|
+
"Unable to read vault file: #{file_path}\n#{error.class}: #{error.message}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def fetch_latest_version
|
|
165
|
+
uri = URI("https://rubygems.org/api/v1/gems/2pass.json")
|
|
166
|
+
response = Net::HTTP.start(
|
|
167
|
+
uri.host,
|
|
168
|
+
uri.port,
|
|
169
|
+
use_ssl: true,
|
|
170
|
+
open_timeout: 5,
|
|
171
|
+
read_timeout: 5
|
|
172
|
+
) do |http|
|
|
173
|
+
http.get(uri.request_uri)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
177
|
+
raise "Unable to check latest version (HTTP #{response.code})"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
JSON.parse(response.body).fetch("version")
|
|
181
|
+
rescue JSON::ParserError, KeyError => e
|
|
182
|
+
raise "Unable to parse latest version from RubyGems: #{e.message}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def run_update_command
|
|
186
|
+
system(*update_command)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def update_command
|
|
190
|
+
[Gem.ruby, "-S", "gem", "update", "2pass"]
|
|
191
|
+
end
|
|
81
192
|
end
|
|
82
193
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: 2pass
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Olivier
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -72,6 +71,20 @@ dependencies:
|
|
|
72
71
|
- - ">="
|
|
73
72
|
- !ruby/object:Gem::Version
|
|
74
73
|
version: 1.3.0
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: terminal-table
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '4.0'
|
|
81
|
+
type: :runtime
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '4.0'
|
|
75
88
|
description: 2pass is a CLI application for managing YAML-based vaults.
|
|
76
89
|
email:
|
|
77
90
|
- contact@yafoy.com
|
|
@@ -91,7 +104,6 @@ homepage: https://2pass.xyz
|
|
|
91
104
|
licenses:
|
|
92
105
|
- MIT
|
|
93
106
|
metadata: {}
|
|
94
|
-
post_install_message:
|
|
95
107
|
rdoc_options: []
|
|
96
108
|
require_paths:
|
|
97
109
|
- lib
|
|
@@ -106,8 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
106
118
|
- !ruby/object:Gem::Version
|
|
107
119
|
version: '0'
|
|
108
120
|
requirements: []
|
|
109
|
-
rubygems_version:
|
|
110
|
-
signing_key:
|
|
121
|
+
rubygems_version: 4.0.3
|
|
111
122
|
specification_version: 4
|
|
112
123
|
summary: A CLI app for managing secrets
|
|
113
124
|
test_files: []
|