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,27 @@
1
+ require 'duse/cli'
2
+ require 'duse/cli/key_helper'
3
+
4
+ module Duse
5
+ module CLI
6
+ class Login < ApiCommand
7
+ include KeyHelper
8
+
9
+ description 'login to access and save secrets'
10
+
11
+ skip :authenticate
12
+
13
+ def run
14
+ username = terminal.ask('Username: ')
15
+ password = terminal.ask('Password: ') { |q| q.echo = 'x' }
16
+ response = Duse.session.post('/users/token', { username: username, password: password })
17
+ config.token = response['api_token']
18
+ CLIConfig.save(config)
19
+ user = Duse::User.find 'me'
20
+ ensure_matching_keys_for user
21
+ success 'Successfully logged in!'
22
+ rescue Duse::Client::NotLoggedIn => e
23
+ error e.message.empty? ? 'Wrong username or password!' : e.message
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Duse
2
+ module CLI
3
+ class MetaCommand < Command
4
+ abstract
5
+
6
+ def run(*sub_commands)
7
+ say help
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,43 @@
1
+ require 'duse/cli'
2
+ require 'optparse'
3
+
4
+ module Duse
5
+ module CLI
6
+ module Parser
7
+ def on_initialize(&block)
8
+ @on_initialize ||= []
9
+ @on_initialize << block if block
10
+ if superclass.respond_to? :on_initialize
11
+ superclass.on_initialize + @on_initialize
12
+ else
13
+ @on_initialize
14
+ end
15
+ end
16
+
17
+ def on(*args, &block)
18
+ block ||= begin
19
+ full_arg = args.detect { |a| a.start_with? '--' }
20
+ name = full_arg.gsub(/^--(\[no-\])?(\S+).*$/, '\2').gsub('-', '_')
21
+ attr_reader(name) unless method_defined? name
22
+ attr_writer(name) unless method_defined? "#{name}="
23
+ alias_method("#{name}?", name) unless method_defined? "#{name}?"
24
+ proc { |instance, value| instance.public_send("#{name}=", value) }
25
+ end
26
+
27
+ on_initialize do |instance|
28
+ instance.parser.on(*args) do |value|
29
+ block.call(instance, value)
30
+ end
31
+ end
32
+ end
33
+
34
+ def new(*)
35
+ attr_accessor :parser unless method_defined? :parser
36
+ result = super
37
+ result.parser = OptionParser.new
38
+ on_initialize.each { |b| b[result] }
39
+ result
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ module Duse
2
+ module PasswordHelper
3
+ def ask_for_password
4
+ loop do
5
+ password = terminal.ask('Password: ') { |q| q.echo = 'x' }
6
+ password_confirmation = terminal.ask('Confirm password: ') { |q| q.echo = 'x' }
7
+ return password if password == password_confirmation
8
+ warn 'Password and password confirmation do not match. Try again.'
9
+ end
10
+ end
11
+
12
+ def ask_for_current_password
13
+ terminal.ask('Current password (to confirm): ') { |q| q.echo = 'x' }
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,45 @@
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 Register < ApiCommand
8
+ include KeyHelper
9
+ include PasswordHelper
10
+
11
+ description 'Register a new account'
12
+
13
+ skip :authenticate
14
+
15
+ def run
16
+ ask_for_user_input
17
+ user = Duse::User.create(
18
+ username: @username,
19
+ email: @email,
20
+ password: @password,
21
+ public_key: @key.public_key.to_pem
22
+ )
23
+ Duse::CLIConfig.new.save_private_key_for user, @key.to_pem
24
+ success 'Successfully created your account! You can now login with "duse login"'
25
+ end
26
+
27
+ private
28
+
29
+ def ask_for_user_input
30
+ @username = choose_username
31
+ @email = choose_email
32
+ @password = ask_for_password
33
+ @key = choose_key
34
+ end
35
+
36
+ def choose_username
37
+ terminal.ask('Username: ')
38
+ end
39
+
40
+ def choose_email
41
+ terminal.ask('Email: ')
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,19 @@
1
+ require 'duse/cli/secret_add'
2
+ require 'duse/cli/secret_get'
3
+ require 'duse/cli/secret_list'
4
+ require 'duse/cli/secret_remove'
5
+ require 'duse/cli/secret_update'
6
+
7
+ module Duse
8
+ module CLI
9
+ class Secret < MetaCommand
10
+ subcommand SecretAdd
11
+ subcommand SecretGet
12
+ subcommand SecretList
13
+ subcommand SecretRemove
14
+ subcommand SecretUpdate
15
+
16
+ description 'Save, retrieve and delete secrets'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ require 'duse/cli'
2
+ require 'openssl'
3
+ require 'duse/cli/key_helper'
4
+ require 'duse/cli/share_with_user'
5
+ require 'duse/cli/secret_generator'
6
+
7
+ module Duse
8
+ module CLI
9
+ class SecretAdd < ApiCommand
10
+ include KeyHelper
11
+ include ShareWithUser
12
+
13
+ description 'Save a new secret'
14
+
15
+ on('-t', '--title [TITLE]', 'The title for the secret to save')
16
+ on('-s', '--secret [SECRET]', 'The secret to save')
17
+ on('-g', '--generate-secret', 'Automatically generate the secret')
18
+ on('-f', '--file [FILE]', 'Read the secret to save from this file')
19
+
20
+ def run
21
+ self.title ||= terminal.ask 'What do you want to call this secret? '
22
+ self.secret = File.read(self.file) if file?
23
+ self.secret = SecretGenerator.new.generated_password if generate_secret?
24
+ self.secret ||= terminal.ask 'Secret to save: '
25
+ users = who_to_share_with
26
+
27
+ user = Duse::User.current
28
+ ensure_matching_keys_for user
29
+ private_key = config.private_key_for user
30
+ secret = Duse::Client::Secret.new title: self.title, secret_text: self.secret, users: users
31
+ secret_hash = Duse::Client::SecretMarshaller.new(secret, private_key).to_h
32
+
33
+ response = Duse::Secret.create secret_hash
34
+ success 'Secret successfully created!'
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,10 @@
1
+ require 'securerandom'
2
+
3
+ module Duse
4
+ class SecretGenerator
5
+ def generated_password
6
+ SecureRandom.base64(32)
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,40 @@
1
+ require 'duse/cli'
2
+ require 'openssl'
3
+ require 'duse/cli/key_helper'
4
+
5
+ module Duse
6
+ module CLI
7
+ class SecretGet < ApiCommand
8
+ include KeyHelper
9
+ description 'Retrieve a secret'
10
+
11
+ on('-p', '--plain', 'Print the decrypted secret plain, without additional information.')
12
+
13
+ def run(secret_id = nil)
14
+ secret_id ||= terminal.ask('Secret to retrieve: ').to_i
15
+
16
+ secret = Duse::Secret.find secret_id
17
+ print_secret(secret)
18
+ end
19
+
20
+ def print_secret(secret)
21
+ user = Duse::User.current
22
+ ensure_matching_keys_for user
23
+ private_key = config.private_key_for user
24
+ plain_secret = secret.decrypt(private_key)
25
+
26
+ if plain?
27
+ print plain_secret
28
+ $stdout.flush
29
+ return
30
+ end
31
+
32
+ say "
33
+ Name: #{secret.title}
34
+ Secret: #{plain_secret}
35
+ Access: #{secret.users.map(&:username).join(', ')}
36
+ ".gsub(/^( |\t)+/, "")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ require 'duse/cli'
2
+
3
+ module Duse
4
+ module CLI
5
+ class SecretList < ApiCommand
6
+ description 'List all secrets you have access to'
7
+
8
+ def run
9
+ secrets = Duse::Secret.all
10
+ secrets.each do |s|
11
+ puts "#{s.id}: #{s.title}"
12
+ end
13
+ if secrets.empty?
14
+ say 'You have not yet saved any secrets, ' \
15
+ 'you can do so with "duse secret save".'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ require 'duse/cli'
2
+
3
+ module Duse
4
+ module CLI
5
+ class SecretRemove < ApiCommand
6
+ description 'Delete a secret'
7
+
8
+ def run(secret_id = nil)
9
+ secret_id ||= terminal.ask('Secret to delete: ').to_i
10
+
11
+ Duse::Secret.delete secret_id
12
+
13
+ success 'Successfully deleted'
14
+ rescue Duse::Client::Error => e
15
+ error e.message
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ require 'duse/cli'
2
+ require 'openssl'
3
+ require 'duse/cli/key_helper'
4
+ require 'duse/cli/share_with_user'
5
+
6
+ module Duse
7
+ module CLI
8
+ class SecretUpdate < ApiCommand
9
+ include KeyHelper
10
+ include ShareWithUser
11
+
12
+ description 'Save a new secret'
13
+
14
+ def run(secret_id = nil)
15
+ secret_id ||= terminal.ask('Secret to update: ').to_i
16
+
17
+ user = Duse::User.current
18
+ ensure_matching_keys_for user
19
+ private_key = config.private_key_for user
20
+ secret = Duse::Secret.find secret_id
21
+ print_secret secret, private_key
22
+ secret = update_secret(secret)
23
+ secret_hash = Duse::Client::SecretMarshaller.new(secret, private_key).to_h
24
+
25
+ response = Duse::Secret.update secret_id, secret_hash
26
+ success 'Secret successfully updated!'
27
+ end
28
+
29
+ private
30
+
31
+ def print_secret(secret, private_key)
32
+ puts "\nName: #{secret.title}"
33
+ puts "Secret: #{secret.decrypt(private_key)}\n"
34
+ end
35
+
36
+ def update_secret(secret)
37
+ title = terminal.ask 'What do you want to call this secret? ' if terminal.agree 'Change the title? '
38
+ secret_text = terminal.ask 'Secret to save: ' if terminal.agree 'Change the secret? '
39
+ users = who_to_share_with if terminal.agree 'Change accessible users? '
40
+ Duse::Client::Secret.new title: title, secret_text: secret_text, users: users
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ class CommaSeparatedIntegerList
2
+ def initialize(string)
3
+ @list = string.split(',').map(&:strip).delete_if(&:empty?).map(&:to_i)
4
+ end
5
+
6
+ def map(&block)
7
+ @list.map(&block)
8
+ end
9
+ end
10
+
11
+ module Duse
12
+ module CLI
13
+ module ShareWithUser
14
+ class InvalidSelection < StandardError; end
15
+
16
+ private
17
+
18
+ def who_to_share_with
19
+ required_users = [Duse::User.find('me'), Duse::User.find('server')]
20
+ wants_to_share = terminal.agree 'Do you want to share this secret?[Y/n] '
21
+ return required_users unless wants_to_share
22
+ required_users + select_users(required_users)
23
+ end
24
+
25
+ def select_users(ignored_users = [])
26
+ users = Duse::User.all
27
+ users = users.delete_if { |u| ignored_users.map(&:id).include? u.id }
28
+ return [] if users.empty?
29
+ terminal.say 'Who do you want to share this secret with?'
30
+ select_from_list(users, :username)
31
+ end
32
+
33
+ def select_from_list(subjects, method = :to_s)
34
+ print_list(subjects, method)
35
+ selection = terminal.ask 'Separate with commas, to select multiple'
36
+ CommaSeparatedIntegerList.new(selection).map do |i|
37
+ fail InvalidSelection if subjects[i-1].nil?
38
+ subjects[i-1]
39
+ end
40
+ rescue InvalidSelection
41
+ warn 'One or more of your selections are invalid. Please try again'
42
+ select_from_list(subjects, method)
43
+ end
44
+
45
+ def print_list(items, method = :to_s)
46
+ items.each_with_index do |item, index|
47
+ terminal.say "#{index+1}: #{item.public_send(method)}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
@@ -0,0 +1,12 @@
1
+ module Duse
2
+ module CLI
3
+ class Version < Command
4
+ description 'print the client version'
5
+
6
+ def run
7
+ say Duse::VERSION
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,36 @@
1
+ require 'uri'
2
+
3
+ module Duse
4
+ module Client
5
+ class Config
6
+ attr_reader :settings
7
+
8
+ def initialize(settings = {})
9
+ @settings = settings
10
+ end
11
+
12
+ def uri=(uri)
13
+ fail ArgumentError, 'Not an uri' unless uri =~ URI.regexp
14
+ settings['uri'] = uri
15
+ end
16
+
17
+ def uri
18
+ settings['uri']
19
+ end
20
+
21
+ def token=(token)
22
+ fail ArgumentError, 'Token must be a string' unless token.is_a? String
23
+ fail ArgumentError, 'Token must not be empty' if token.empty?
24
+ settings['token'] = token
25
+ end
26
+
27
+ def token
28
+ settings['token']
29
+ end
30
+
31
+ def to_h
32
+ settings.clone
33
+ end
34
+ end
35
+ end
36
+ end