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,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,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,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,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
|