duse 0.0.2
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 +15 -0
- data/.gitignore +35 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +13 -0
- data/LICENSE +22 -0
- data/README.md +89 -0
- data/Rakefile +11 -0
- data/bin/duse +8 -0
- data/duse.gemspec +22 -0
- data/lib/duse.rb +10 -0
- data/lib/duse/cli.rb +104 -0
- data/lib/duse/cli/account.rb +20 -0
- data/lib/duse/cli/account_confirm.rb +22 -0
- data/lib/duse/cli/account_info.rb +18 -0
- data/lib/duse/cli/account_password.rb +14 -0
- data/lib/duse/cli/account_password_change.rb +30 -0
- data/lib/duse/cli/account_password_reset.rb +20 -0
- data/lib/duse/cli/account_resend_confirmation.rb +20 -0
- data/lib/duse/cli/account_update.rb +25 -0
- data/lib/duse/cli/api_command.rb +33 -0
- data/lib/duse/cli/cli_config.rb +54 -0
- data/lib/duse/cli/command.rb +202 -0
- data/lib/duse/cli/config.rb +16 -0
- data/lib/duse/cli/help.rb +23 -0
- data/lib/duse/cli/key_helper.rb +84 -0
- data/lib/duse/cli/login.rb +27 -0
- data/lib/duse/cli/meta_command.rb +12 -0
- data/lib/duse/cli/parser.rb +43 -0
- data/lib/duse/cli/password_helper.rb +17 -0
- data/lib/duse/cli/register.rb +45 -0
- data/lib/duse/cli/secret.rb +19 -0
- data/lib/duse/cli/secret_add.rb +38 -0
- data/lib/duse/cli/secret_generator.rb +10 -0
- data/lib/duse/cli/secret_get.rb +40 -0
- data/lib/duse/cli/secret_list.rb +20 -0
- data/lib/duse/cli/secret_remove.rb +19 -0
- data/lib/duse/cli/secret_update.rb +44 -0
- data/lib/duse/cli/share_with_user.rb +53 -0
- data/lib/duse/cli/version.rb +12 -0
- data/lib/duse/client/config.rb +36 -0
- data/lib/duse/client/entity.rb +87 -0
- data/lib/duse/client/namespace.rb +112 -0
- data/lib/duse/client/secret.rb +69 -0
- data/lib/duse/client/session.rb +128 -0
- data/lib/duse/client/user.rb +23 -0
- data/lib/duse/encryption.rb +39 -0
- data/lib/duse/version.rb +3 -0
- data/spec/cli/cli_config_spec.rb +49 -0
- data/spec/cli/commands/account_spec.rb +45 -0
- data/spec/cli/commands/config_spec.rb +17 -0
- data/spec/cli/commands/login_spec.rb +51 -0
- data/spec/cli/commands/register_spec.rb +38 -0
- data/spec/cli/commands/secret_spec.rb +142 -0
- data/spec/client/secret_marshaller_spec.rb +32 -0
- data/spec/client/secret_spec.rb +96 -0
- data/spec/client/user_spec.rb +105 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/support/helpers.rb +43 -0
- data/spec/support/mock_api.rb +142 -0
- metadata +159 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'duse/cli'
|
2
|
+
require 'duse/cli/password_helper'
|
3
|
+
|
4
|
+
module Duse
|
5
|
+
module CLI
|
6
|
+
class AccountPasswordChange < ApiCommand
|
7
|
+
include PasswordHelper
|
8
|
+
|
9
|
+
description 'change your password'
|
10
|
+
|
11
|
+
on('-t', '--token [TOKEN]', 'The token to use for password reset')
|
12
|
+
|
13
|
+
def run
|
14
|
+
if self.token
|
15
|
+
Duse.session.patch('/users/password', {
|
16
|
+
token: self.token,
|
17
|
+
password: ask_for_password
|
18
|
+
})
|
19
|
+
else
|
20
|
+
user = Duse::User.current
|
21
|
+
Duse::User.update(user.id, {
|
22
|
+
password: ask_for_password,
|
23
|
+
current_password: ask_for_current_password
|
24
|
+
})
|
25
|
+
end
|
26
|
+
success 'Successfully updated your password!'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'duse/cli'
|
2
|
+
|
3
|
+
module Duse
|
4
|
+
module CLI
|
5
|
+
class AccountPasswordReset < ApiCommand
|
6
|
+
description 'request a reset of your password'
|
7
|
+
|
8
|
+
skip :authenticate
|
9
|
+
|
10
|
+
on('-e', '--email [EMAIL]', 'Your email')
|
11
|
+
|
12
|
+
def run
|
13
|
+
self.email ||= terminal.ask('Your email: ')
|
14
|
+
Duse::User.forgot_password(self.email)
|
15
|
+
success 'An email with instructions on how to reset your password has been sent.'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'duse/cli'
|
2
|
+
|
3
|
+
module Duse
|
4
|
+
module CLI
|
5
|
+
class AccountResendConfirmation < ApiCommand
|
6
|
+
description 'resend the confirmation email for your account'
|
7
|
+
|
8
|
+
skip :authenticate
|
9
|
+
|
10
|
+
on('-e', '--email [EMAIL]', 'Your email')
|
11
|
+
|
12
|
+
def run
|
13
|
+
self.email ||= terminal.ask('Your email: ')
|
14
|
+
Duse::User.resend_confirmation(self.email)
|
15
|
+
success 'New confirmation process started.'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'duse/cli'
|
2
|
+
require 'duse/cli/key_helper'
|
3
|
+
require 'duse/cli/password_helper'
|
4
|
+
|
5
|
+
module Duse
|
6
|
+
module CLI
|
7
|
+
class AccountUpdate < ApiCommand
|
8
|
+
include KeyHelper
|
9
|
+
include PasswordHelper
|
10
|
+
|
11
|
+
description 'update account'
|
12
|
+
|
13
|
+
def run
|
14
|
+
user = Duse::User.current
|
15
|
+
terminal.say 'Leave blank if you do not wish to change'
|
16
|
+
Duse::User.update(user.id, {
|
17
|
+
username: terminal.ask('Username: ') { |q| q.default = user.username },
|
18
|
+
email: terminal.ask('Email: ') { |q| q.default = user.email },
|
19
|
+
current_password: ask_for_current_password
|
20
|
+
})
|
21
|
+
success 'Successfully updated your account!'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Duse
|
4
|
+
module CLI
|
5
|
+
class ApiCommand < Command
|
6
|
+
abstract
|
7
|
+
|
8
|
+
def execute
|
9
|
+
ensure_uri_is_set
|
10
|
+
authenticate
|
11
|
+
run *arguments
|
12
|
+
rescue Duse::Client::NotLoggedIn
|
13
|
+
error "not logged in, run `#$0 login`"
|
14
|
+
rescue Duse::Client::Error => e
|
15
|
+
error e.message
|
16
|
+
rescue Faraday::ConnectionFailed
|
17
|
+
error 'Cannot connect to specified duse instance'
|
18
|
+
rescue Interrupt
|
19
|
+
say "\naborted!"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def ensure_uri_is_set
|
25
|
+
error "client not configured, run `#$0 config`" if config.uri.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def authenticate
|
29
|
+
fail Duse::Client::NotLoggedIn if config.token.nil?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'duse/client/config'
|
3
|
+
|
4
|
+
module Duse
|
5
|
+
class PrivateKeyMissing < StandardError; end
|
6
|
+
|
7
|
+
class CLIConfig < Duse::Client::Config
|
8
|
+
def private_key_for(user)
|
9
|
+
private_key_filename = private_key_file_for user
|
10
|
+
fail PrivateKeyMissing unless File.exists? private_key_filename
|
11
|
+
OpenSSL::PKey::RSA.new File.read private_key_filename
|
12
|
+
end
|
13
|
+
|
14
|
+
def save_private_key_for(user, private_key)
|
15
|
+
File.open(private_key_file_for(user), 'w') do |file|
|
16
|
+
file.write private_key
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.load
|
21
|
+
config = YAML.load load_config_file
|
22
|
+
return {} unless config.is_a? Hash
|
23
|
+
config
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.save(config)
|
27
|
+
FileUtils.mkdir_p config_dir
|
28
|
+
File.open(config_file, 'w') do |file|
|
29
|
+
file.write config.to_h.to_yaml
|
30
|
+
file.chmod 0600
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.config_file
|
35
|
+
File.join config_dir, 'config.yml'
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.config_dir
|
39
|
+
File.join Dir.home, '.config', 'duse'
|
40
|
+
end
|
41
|
+
|
42
|
+
def private_key_file_for(user)
|
43
|
+
File.join self.class.config_dir, user.username
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def self.load_config_file
|
49
|
+
# pretend like it's an empty file if config does not exists
|
50
|
+
return "--- {}\n" unless File.exist? config_file
|
51
|
+
File.read config_file
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'highline'
|
2
|
+
require 'duse/cli/cli_config'
|
3
|
+
require 'duse/cli/parser'
|
4
|
+
|
5
|
+
module Duse
|
6
|
+
module CLI
|
7
|
+
class Command
|
8
|
+
attr_reader :output, :input
|
9
|
+
attr_accessor :arguments, :force_interactive
|
10
|
+
|
11
|
+
extend Parser
|
12
|
+
|
13
|
+
HighLine.color_scheme = HighLine::ColorScheme.new do |cs|
|
14
|
+
cs[:command] = [ :bold ]
|
15
|
+
cs[:error] = [ :red ]
|
16
|
+
cs[:important] = [ :bold, :underline ]
|
17
|
+
cs[:success] = [ :green ]
|
18
|
+
cs[:info] = [ :yellow ]
|
19
|
+
cs[:debug] = [ :magenta ]
|
20
|
+
end
|
21
|
+
|
22
|
+
on('-h', '--help', 'Display help') do |c, _|
|
23
|
+
c.say c.help
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(options = {})
|
28
|
+
self.output = $stdout
|
29
|
+
self.input = $stdin
|
30
|
+
self.arguments ||= []
|
31
|
+
options.each do |key, value|
|
32
|
+
public_send("#{key}=", value) if respond_to? "#{key}="
|
33
|
+
end
|
34
|
+
@arguments ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse(args)
|
38
|
+
arguments.concat(parser.parse(args))
|
39
|
+
rescue OptionParser::ParseError => e
|
40
|
+
error e.message
|
41
|
+
end
|
42
|
+
|
43
|
+
def execute
|
44
|
+
run *arguments
|
45
|
+
rescue Interrupt
|
46
|
+
say "\naborted!"
|
47
|
+
end
|
48
|
+
|
49
|
+
def output=(io)
|
50
|
+
@terminal = nil
|
51
|
+
@output = io
|
52
|
+
end
|
53
|
+
|
54
|
+
def input=(io)
|
55
|
+
@terminal = nil
|
56
|
+
@input = io
|
57
|
+
end
|
58
|
+
|
59
|
+
def terminal
|
60
|
+
@terminal ||= HighLine.new(input, output)
|
61
|
+
end
|
62
|
+
|
63
|
+
# ignore Command since its not supposed to be executed
|
64
|
+
@@abstract ||= [Command]
|
65
|
+
def self.abstract?
|
66
|
+
@@abstract.include? self
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.abstract
|
70
|
+
@@abstract << self
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.skip(*names)
|
74
|
+
names.each { |n| define_method(n) {} }
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.super_command=(command_class)
|
78
|
+
@super_command = command_class
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.has_super_command?
|
82
|
+
!@super_command.nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.super_command
|
86
|
+
@super_command
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.subcommand(command)
|
90
|
+
return nil if command.nil?
|
91
|
+
return subcommands.select { |sc| sc.command_name == command }.first if command.is_a? String
|
92
|
+
command.super_command = self
|
93
|
+
subcommands << command
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.subcommands
|
97
|
+
@subcommands ||= []
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.has_subcommands?
|
101
|
+
!@subcommands.empty?
|
102
|
+
end
|
103
|
+
|
104
|
+
def config
|
105
|
+
CLI.config
|
106
|
+
end
|
107
|
+
|
108
|
+
def warn(message)
|
109
|
+
write_to($stderr) do
|
110
|
+
say color(message, :error)
|
111
|
+
yield if block_given?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def error(message, &block)
|
116
|
+
warn(message, &block)
|
117
|
+
exit 1
|
118
|
+
end
|
119
|
+
|
120
|
+
def success(line)
|
121
|
+
say color(line, :success) if interactive?
|
122
|
+
end
|
123
|
+
|
124
|
+
def write_to(io)
|
125
|
+
io_was, self.output = output, io
|
126
|
+
yield
|
127
|
+
ensure
|
128
|
+
self.output = io_was if io_was
|
129
|
+
end
|
130
|
+
|
131
|
+
def color(line, style)
|
132
|
+
return line.to_s unless interactive?
|
133
|
+
terminal.color(line || '???', Array(style).map(&:to_sym))
|
134
|
+
end
|
135
|
+
|
136
|
+
def interactive?(io = output)
|
137
|
+
return io.tty? if force_interactive.nil?
|
138
|
+
force_interactive
|
139
|
+
end
|
140
|
+
|
141
|
+
def say(data, format = nil, style = nil)
|
142
|
+
terminal.say format(data, format, style)
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.full_command
|
146
|
+
name[/[^:]*$/].split(/(?=[A-Z])/).map(&:downcase).join(' ')
|
147
|
+
end
|
148
|
+
|
149
|
+
def full_command
|
150
|
+
self.class.full_command
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.command_name
|
154
|
+
return full_command.sub(super_command.full_command, '').strip.sub(' ', '-') if has_super_command?
|
155
|
+
full_command
|
156
|
+
end
|
157
|
+
|
158
|
+
def command_name
|
159
|
+
self.class.command_name
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.description(description = nil)
|
163
|
+
@description = description if description
|
164
|
+
@description ||= ""
|
165
|
+
end
|
166
|
+
|
167
|
+
def help(info = "")
|
168
|
+
return help_subcommands unless self.class.subcommands.empty?
|
169
|
+
parser.banner = usage
|
170
|
+
self.class.description.sub(/./) { |c| c.upcase } + ".\n\n" + info + parser.to_s
|
171
|
+
end
|
172
|
+
|
173
|
+
def help_subcommands
|
174
|
+
result = "#{self.class.description}\n\n"
|
175
|
+
result << "Usage: duse #{full_command} COMMAND ...\n\nAvailable commands:\n\n"
|
176
|
+
self.class.subcommands.each { |command_class| result << "\t#{color(command_class.command_name, :command).ljust(22)} #{color(command_class.description, :info)}\n" }
|
177
|
+
result << "\nrun `#$0 help #{full_command} COMMAND` for more infos"
|
178
|
+
result
|
179
|
+
end
|
180
|
+
|
181
|
+
def usage
|
182
|
+
"Usage: " << color(usage_for(full_command, :run), :command)
|
183
|
+
end
|
184
|
+
|
185
|
+
def usage_for(prefix, method)
|
186
|
+
usage = "duse #{prefix}"
|
187
|
+
method = method(method)
|
188
|
+
if method.respond_to? :parameters
|
189
|
+
method.parameters.each do |type, name|
|
190
|
+
name = name.upcase
|
191
|
+
name = "[#{name}]" if type == :opt
|
192
|
+
name = "[#{name}..]" if type == :rest
|
193
|
+
usage << " #{name}"
|
194
|
+
end
|
195
|
+
elsif method.arity != 0
|
196
|
+
usage << " ..."
|
197
|
+
end
|
198
|
+
usage << " [OPTIONS]"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'duse/cli'
|
2
|
+
|
3
|
+
module Duse
|
4
|
+
module CLI
|
5
|
+
class Config < Command
|
6
|
+
description 'Configure the client'
|
7
|
+
|
8
|
+
def run
|
9
|
+
config.uri = terminal.ask('Uri to the duse instance you want to use: ') { |q| q.default = config.uri }
|
10
|
+
CLIConfig.save(config)
|
11
|
+
rescue ArgumentError
|
12
|
+
error 'Not an uri'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'duse/cli'
|
2
|
+
|
3
|
+
module Duse
|
4
|
+
module CLI
|
5
|
+
class Help < Command
|
6
|
+
description "Displays help messages, such as this one"
|
7
|
+
|
8
|
+
def run(*args)
|
9
|
+
unless args.empty?
|
10
|
+
say CLI.command(args).new.help
|
11
|
+
else
|
12
|
+
say "Usage: duse COMMAND ...\n\nAvailable commands:\n\n"
|
13
|
+
commands.each { |c| say "\t#{color(c.command_name, :command).ljust(22)} #{color(c.description, :info)}" }
|
14
|
+
say "\nrun `#$0 help COMMAND` for more infos"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def commands
|
19
|
+
CLI.commands.sort_by { |c| c.command_name }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Duse
|
2
|
+
module KeyHelper
|
3
|
+
def choose_key(options = { allow_generate: true })
|
4
|
+
key = nil
|
5
|
+
generate_option = 'Generate a new one'
|
6
|
+
choose_myself_option = 'Let me choose it myself'
|
7
|
+
choices = possible_ssh_keys
|
8
|
+
choices << generate_option if options[:allow_generate]
|
9
|
+
choices << choose_myself_option
|
10
|
+
terminal.choose do |ssh_keys|
|
11
|
+
ssh_keys.prompt = 'Which private ssh-key do you want to use?'
|
12
|
+
ssh_keys.choices *choices do |choice|
|
13
|
+
key = generate_key if choice == generate_option
|
14
|
+
key = choose_private_key_file if choice == choose_myself_option
|
15
|
+
key ||= OpenSSL::PKey::RSA.new File.read choice
|
16
|
+
end
|
17
|
+
end
|
18
|
+
key
|
19
|
+
end
|
20
|
+
|
21
|
+
def ensure_matching_keys_for(user)
|
22
|
+
key_pair = {
|
23
|
+
public: user.public_key,
|
24
|
+
private: private_key_for(user)
|
25
|
+
}
|
26
|
+
loop do
|
27
|
+
break if matching_keys? key_pair
|
28
|
+
warn 'Your private key does not match the public key, please select a new one.'
|
29
|
+
key_pair[:private] = choose_key(allow_generate: false)
|
30
|
+
end
|
31
|
+
Duse::CLIConfig.new.save_private_key_for user, key_pair[:private].to_pem
|
32
|
+
end
|
33
|
+
|
34
|
+
def private_key_for(user)
|
35
|
+
Duse::CLIConfig.new.private_key_for(user)
|
36
|
+
rescue PrivateKeyMissing
|
37
|
+
warn 'No private key found, please select one.'
|
38
|
+
choose_key(allow_generate: false)
|
39
|
+
rescue OpenSSL::PKey::RSAError
|
40
|
+
warn 'The private key file does not contain a valid private key, please select a new one.'
|
41
|
+
choose_key(allow_generate: false)
|
42
|
+
end
|
43
|
+
|
44
|
+
def matching_keys?(key_pair)
|
45
|
+
public_key = key_pair[:public]
|
46
|
+
private_key = key_pair[:private]
|
47
|
+
public_key.params['n'] == private_key.params['n']
|
48
|
+
end
|
49
|
+
|
50
|
+
def possible_ssh_keys
|
51
|
+
ssh_keys = Dir.glob File.join(ssh_dir, '*')
|
52
|
+
ssh_keys.keep_if &method(:valid_ssh_private_key?)
|
53
|
+
end
|
54
|
+
|
55
|
+
def choose_private_key_file
|
56
|
+
private_key_file = nil
|
57
|
+
loop do
|
58
|
+
private_key_file = terminal.ask('Private key file: ') { |q| q.default = File.join ssh_dir, 'id_rsa' }
|
59
|
+
break if valid_ssh_private_key? private_key_file
|
60
|
+
end
|
61
|
+
OpenSSL::PKey::RSA.new File.read private_key_file
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid_ssh_private_key?(private_key_file)
|
65
|
+
rsa_key = OpenSSL::PKey::RSA.new File.read private_key_file
|
66
|
+
rsa_key.private?
|
67
|
+
rescue
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def generate_key
|
72
|
+
OpenSSL::PKey::RSA.generate 4096
|
73
|
+
end
|
74
|
+
|
75
|
+
def ssh_dir
|
76
|
+
File.join home_dir, '.ssh'
|
77
|
+
end
|
78
|
+
|
79
|
+
def home_dir
|
80
|
+
File.expand_path '~'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|