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.
Files changed (61) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +35 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +13 -0
  6. data/LICENSE +22 -0
  7. data/README.md +89 -0
  8. data/Rakefile +11 -0
  9. data/bin/duse +8 -0
  10. data/duse.gemspec +22 -0
  11. data/lib/duse.rb +10 -0
  12. data/lib/duse/cli.rb +104 -0
  13. data/lib/duse/cli/account.rb +20 -0
  14. data/lib/duse/cli/account_confirm.rb +22 -0
  15. data/lib/duse/cli/account_info.rb +18 -0
  16. data/lib/duse/cli/account_password.rb +14 -0
  17. data/lib/duse/cli/account_password_change.rb +30 -0
  18. data/lib/duse/cli/account_password_reset.rb +20 -0
  19. data/lib/duse/cli/account_resend_confirmation.rb +20 -0
  20. data/lib/duse/cli/account_update.rb +25 -0
  21. data/lib/duse/cli/api_command.rb +33 -0
  22. data/lib/duse/cli/cli_config.rb +54 -0
  23. data/lib/duse/cli/command.rb +202 -0
  24. data/lib/duse/cli/config.rb +16 -0
  25. data/lib/duse/cli/help.rb +23 -0
  26. data/lib/duse/cli/key_helper.rb +84 -0
  27. data/lib/duse/cli/login.rb +27 -0
  28. data/lib/duse/cli/meta_command.rb +12 -0
  29. data/lib/duse/cli/parser.rb +43 -0
  30. data/lib/duse/cli/password_helper.rb +17 -0
  31. data/lib/duse/cli/register.rb +45 -0
  32. data/lib/duse/cli/secret.rb +19 -0
  33. data/lib/duse/cli/secret_add.rb +38 -0
  34. data/lib/duse/cli/secret_generator.rb +10 -0
  35. data/lib/duse/cli/secret_get.rb +40 -0
  36. data/lib/duse/cli/secret_list.rb +20 -0
  37. data/lib/duse/cli/secret_remove.rb +19 -0
  38. data/lib/duse/cli/secret_update.rb +44 -0
  39. data/lib/duse/cli/share_with_user.rb +53 -0
  40. data/lib/duse/cli/version.rb +12 -0
  41. data/lib/duse/client/config.rb +36 -0
  42. data/lib/duse/client/entity.rb +87 -0
  43. data/lib/duse/client/namespace.rb +112 -0
  44. data/lib/duse/client/secret.rb +69 -0
  45. data/lib/duse/client/session.rb +128 -0
  46. data/lib/duse/client/user.rb +23 -0
  47. data/lib/duse/encryption.rb +39 -0
  48. data/lib/duse/version.rb +3 -0
  49. data/spec/cli/cli_config_spec.rb +49 -0
  50. data/spec/cli/commands/account_spec.rb +45 -0
  51. data/spec/cli/commands/config_spec.rb +17 -0
  52. data/spec/cli/commands/login_spec.rb +51 -0
  53. data/spec/cli/commands/register_spec.rb +38 -0
  54. data/spec/cli/commands/secret_spec.rb +142 -0
  55. data/spec/client/secret_marshaller_spec.rb +32 -0
  56. data/spec/client/secret_spec.rb +96 -0
  57. data/spec/client/user_spec.rb +105 -0
  58. data/spec/spec_helper.rb +70 -0
  59. data/spec/support/helpers.rb +43 -0
  60. data/spec/support/mock_api.rb +142 -0
  61. 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
+