ccli 0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b51dfcdf9a76d67d7bb633b23527d834e1eefeed029b4d8cce842ce1d114126c
4
+ data.tar.gz: e4aaff7ebd82de60040209ee625b7e4b479d84ffff1f2eeed3f9d919ea48ac83
5
+ SHA512:
6
+ metadata.gz: c325b131a7eacbee13c0c6caa99288876958ee5a1424a7a14cbff80d13fdc3d8f7886f4f46d57b483fa6ee47c2e019497e72a75d223360698079c59d6db8e138
7
+ data.tar.gz: ec80bdf42056d9101b64a7eb99ec991b7aef7a7007a9481ab714253455e260c777af1d42b26765c250c3e9343f84a716b86c860c4c6f677fa9682a5a0e198eb3
@@ -0,0 +1,121 @@
1
+ AllCops:
2
+ DisplayCopNames: true
3
+ Exclude:
4
+ - spec/**/*
5
+
6
+ Metrics/AbcSize:
7
+ Max: 20
8
+ Severity: error
9
+
10
+ Metrics/ClassLength:
11
+ Max: 200
12
+ Severity: error
13
+
14
+ Metrics/ModuleLength:
15
+ Max: 200
16
+ Severity: error
17
+
18
+ Metrics/CyclomaticComplexity:
19
+ Max: 6
20
+ Severity: error
21
+
22
+ Layout/LineLength:
23
+ Max: 100
24
+ Severity: warning
25
+ AutoCorrect: true
26
+
27
+ Metrics/MethodLength:
28
+ Max: 10
29
+ Severity: error
30
+
31
+ Metrics/ParameterLists:
32
+ Max: 6
33
+ Severity: warning
34
+
35
+ Layout/ClassStructure:
36
+ Enabled: true
37
+
38
+ # controller#entry methods have @model_name instance variables.
39
+ # therefore disable this cop
40
+ Naming/MemoizedInstanceVariableName:
41
+ Enabled: false
42
+
43
+ # Keep for now, easier with superclass definitions
44
+ Style/ClassAndModuleChildren:
45
+ Enabled: false
46
+
47
+ # The ones we use must exist for the entire class hierarchy.
48
+ Style/ClassVars:
49
+ Enabled: false
50
+
51
+ Style/EmptyMethod:
52
+ EnforcedStyle: expanded
53
+
54
+ # We thinks that's fine
55
+ Style/FormatStringToken:
56
+ Enabled: false
57
+
58
+
59
+ Style/HashSyntax:
60
+ Exclude:
61
+ - lib/tasks/**/*.rake
62
+
63
+ Style/SymbolArray:
64
+ EnforcedStyle: brackets
65
+
66
+ # map instead of collect, reduce instead of inject.
67
+ # Probably later
68
+ Style/CollectionMethods:
69
+ Enabled: false
70
+
71
+ # Well, well, well
72
+ Style/Documentation:
73
+ Enabled: false
74
+
75
+ # Probably later
76
+ Layout/DotPosition:
77
+ Enabled: false
78
+
79
+ # Missing UTF-8 encoding statements should always be created.
80
+ Style/Encoding:
81
+ Severity: error
82
+
83
+ # Keep single line bodys for if and unless
84
+ Style/IfUnlessModifier:
85
+ Enabled: false
86
+
87
+ # That's no huge stopper
88
+ Layout/EmptyLines:
89
+ Enabled: false
90
+
91
+ # We thinks that's fine for specs
92
+ Layout/EmptyLinesAroundBlockBody:
93
+ Enabled: false
94
+
95
+ # We thinks that's fine
96
+ Layout/EmptyLinesAroundClassBody:
97
+ Enabled: false
98
+
99
+ # We thinks that's fine
100
+ Layout/EmptyLinesAroundModuleBody:
101
+ Enabled: false
102
+
103
+ # We thinks that's fine
104
+ Layout/MultilineOperationIndentation:
105
+ Enabled: false
106
+
107
+ # We thinks that's fine
108
+ Style/RegexpLiteral:
109
+ Enabled: false
110
+
111
+ # We think that's the developers choice
112
+ Style/SymbolProc:
113
+ Enabled: false
114
+
115
+ # Probably later
116
+ Style/GuardClause:
117
+ Enabled: false
118
+
119
+ # We thinks that's fine
120
+ Style/SingleLineBlockParams:
121
+ Enabled: false
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.5
4
+ install:
5
+ - gem install bundler
6
+ - bundle install
7
+ script:
8
+ - rubocop
9
+ - rspec
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'commander', '~> 4.5', '>= 4.5.2'
6
+ gem 'rspec', '~> 3.9'
7
+ gem 'rubocop', '~> 0.89.0'
8
+ gem 'tty-command'
9
+ gem 'tty-exit'
10
+ gem 'tty-logger'
11
+
12
+ gem 'pry'
13
+ gem 'pry-byebug'
@@ -0,0 +1,75 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ast (2.4.1)
5
+ byebug (11.1.3)
6
+ coderay (1.1.3)
7
+ commander (4.5.2)
8
+ highline (~> 2.0.0)
9
+ diff-lcs (1.4.4)
10
+ equatable (0.6.1)
11
+ highline (2.0.3)
12
+ method_source (1.0.0)
13
+ parallel (1.19.2)
14
+ parser (2.7.1.4)
15
+ ast (~> 2.4.1)
16
+ pastel (0.7.4)
17
+ equatable (~> 0.6)
18
+ tty-color (~> 0.5)
19
+ pry (0.13.1)
20
+ coderay (~> 1.1)
21
+ method_source (~> 1.0)
22
+ pry-byebug (3.9.0)
23
+ byebug (~> 11.0)
24
+ pry (~> 0.13.0)
25
+ rainbow (3.0.0)
26
+ regexp_parser (1.7.1)
27
+ rexml (3.2.4)
28
+ rspec (3.9.0)
29
+ rspec-core (~> 3.9.0)
30
+ rspec-expectations (~> 3.9.0)
31
+ rspec-mocks (~> 3.9.0)
32
+ rspec-core (3.9.2)
33
+ rspec-support (~> 3.9.3)
34
+ rspec-expectations (3.9.2)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.9.0)
37
+ rspec-mocks (3.9.1)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.9.0)
40
+ rspec-support (3.9.3)
41
+ rubocop (0.89.0)
42
+ parallel (~> 1.10)
43
+ parser (>= 2.7.1.1)
44
+ rainbow (>= 2.2.2, < 4.0)
45
+ regexp_parser (>= 1.7)
46
+ rexml
47
+ rubocop-ast (>= 0.1.0, < 1.0)
48
+ ruby-progressbar (~> 1.7)
49
+ unicode-display_width (>= 1.4.0, < 2.0)
50
+ rubocop-ast (0.3.0)
51
+ parser (>= 2.7.1.4)
52
+ ruby-progressbar (1.10.1)
53
+ tty-color (0.5.2)
54
+ tty-command (0.9.0)
55
+ pastel (~> 0.7.0)
56
+ tty-exit (0.1.0)
57
+ tty-logger (0.3.0)
58
+ pastel (~> 0.7.0)
59
+ unicode-display_width (1.7.0)
60
+
61
+ PLATFORMS
62
+ ruby
63
+
64
+ DEPENDENCIES
65
+ commander (~> 4.5, >= 4.5.2)
66
+ pry
67
+ pry-byebug
68
+ rspec (~> 3.9)
69
+ rubocop (~> 0.89.0)
70
+ tty-command
71
+ tty-exit
72
+ tty-logger
73
+
74
+ BUNDLED WITH
75
+ 2.1.4
@@ -0,0 +1,63 @@
1
+ # ccli
2
+
3
+ Cryptopus Command Line Client
4
+
5
+ ## Installation
6
+
7
+ `sudo gem install ccli`
8
+
9
+ This will install the `cry` command including its dependencies
10
+
11
+ ## Features
12
+
13
+ - Fetch account data from Cryptopus
14
+ - List accessable teams in Cryptopus
15
+ - Sync Openshift/Kubernetes Secrets to Cryptopus
16
+ - Sync Secrets from Cryptopus to Openshift/Kubernetes
17
+
18
+ ## Usage
19
+
20
+ ### Labeling secret to be synced
21
+
22
+ So that a secret even gets considered by the `ccli`, you have to add the `cryptopus-sync=true` label to your secret:
23
+
24
+ **oc:** `oc label secret <secret-name> cryptopus-sync=true`
25
+
26
+
27
+ **kubectl:** `kubectl label secret <secret-name> cryptopus-sync=true`
28
+
29
+ ### Commands
30
+
31
+ ```
32
+ Command: Summary:
33
+
34
+ account Fetches an account by the given id
35
+ folder Selects the Cryptopus folder by id
36
+ help Display global or [command] help documentation
37
+ k8s-secret-pull Pulls secret from Kubectl to Cryptopus
38
+ k8s-secret-push Pushes secret from Cryptopus to Kubectl
39
+ login Logs in to the ccli
40
+ logout Logs out of the ccli
41
+ ose-secret-pull Pulls secret from Openshift to Cryptopus
42
+ ose-secret-push Pushes secret from Cryptopus to Openshift
43
+ teams Lists all available teams
44
+ use Select the current folder
45
+ ```
46
+
47
+ Show more specific documentation by calling `cry help <command>`
48
+
49
+
50
+ ## Development
51
+
52
+ ### Prerequisites
53
+
54
+ You will need the following things properly installed on your computer:
55
+
56
+ - [Git (Version Control System)](http://git-scm.com/)
57
+ - [RVM (Ruby Version Manager)](http://rvm.io/)
58
+
59
+ ### Setup
60
+
61
+ - `rvm install 2.6.0`
62
+ - `gem install bundler`
63
+ - `bundle install`
data/bin/cry ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/cli'
5
+
6
+ CLI.new.run
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'ccli'
8
+ s.version = '0.1.0'
9
+ s.summary = 'Command line client for the opensource password manager Cryptopus'
10
+ s.authors = ['Nils Rauch']
11
+ s.email = 'rauch@puzzle.ch'
12
+ s.require_paths = ['lib']
13
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
14
+ f.match(%r{(^(test|spec|features)/)})
15
+ end
16
+ s.bindir = 'bin'
17
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ s.required_ruby_version = Gem::Requirement.new('>= 2.0')
19
+
20
+ s.add_runtime_dependency 'commander', '~> 4.5', '>= 4.5.2'
21
+ s.add_runtime_dependency 'tty-command'
22
+ s.add_runtime_dependency 'tty-exit'
23
+ s.add_runtime_dependency 'tty-logger'
24
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-command'
4
+
5
+ class ClusterSecretAdapter
6
+
7
+ CCLI_FLAG_LABEL = 'cryptopus-sync'
8
+
9
+ def fetch_secret(name)
10
+ raise client_missing_error unless client_installed?
11
+ raise client_not_logged_in_error unless client_logged_in?
12
+
13
+ begin
14
+ out, _err = cmd.run("#{client} get -o yaml secret --field-selector='metadata.name=#{name}' " \
15
+ "-l #{CCLI_FLAG_LABEL}=true")
16
+
17
+ Psych.load(out)['items'].first.to_yaml
18
+ rescue TTY::Command::ExitError
19
+ raise OpenshiftSecretNotFoundError
20
+ end
21
+ end
22
+
23
+ def fetch_all_secrets
24
+ raise client_missing_error unless client_installed?
25
+ raise client_not_logged_in_error unless client_logged_in?
26
+
27
+ secrets, _err = cmd.run("#{client} get secret -o yaml -l #{CCLI_FLAG_LABEL}=true")
28
+ Psych.load(secrets)['items'].map do |secret|
29
+ secret.to_yaml
30
+ end
31
+ end
32
+
33
+ def insert_secret(secret)
34
+ raise client_missing_error unless client_installed?
35
+ raise client_not_logged_in_error unless client_logged_in?
36
+
37
+ File.open("/tmp/#{secret.name}.yml", 'w') do |file|
38
+ file.write secret.ose_secret
39
+ end
40
+
41
+ cmd.run("#{client} delete -f /tmp/#{secret.name}.yml --ignore-not-found=true")
42
+ cmd.run("#{client} create -f /tmp/#{secret.name}.yml")
43
+ end
44
+
45
+ private
46
+
47
+ def client_installed?
48
+ cmd.run!("which #{client}").success?
49
+ end
50
+
51
+ def client_logged_in?
52
+ cmd.run!("#{client} get secret").success?
53
+ end
54
+
55
+ def cmd
56
+ @cmd ||= TTY::Command.new(printer: :null)
57
+ end
58
+
59
+ def client
60
+ raise 'implement in subclass'
61
+ end
62
+
63
+ def client_missing_error
64
+ raise 'implement in subclass'
65
+ end
66
+
67
+ def client_not_logged_in_error
68
+ raise 'implement in subclass'
69
+ end
70
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'net/http'
5
+ require 'json'
6
+ require 'base64'
7
+
8
+ class CryptopusAdapter
9
+
10
+ def root_url
11
+ raise SessionMissingError unless session_adapter.session_data[:url]
12
+
13
+ @root_url ||= "#{session_adapter.session_data[:url]}/api"
14
+ end
15
+
16
+ def get(path)
17
+ uri = URI("#{root_url}/#{path}")
18
+ request = new_request(:get, uri)
19
+ send_request(request, uri)
20
+ end
21
+
22
+ def post(path, body)
23
+ uri = URI("#{root_url}/#{path}")
24
+ request = new_request(:post, uri)
25
+ request.body = body
26
+ send_request(request, uri)
27
+ end
28
+
29
+ def patch(path, body)
30
+ uri = URI("#{root_url}/#{path}")
31
+ request = new_request(:patch, uri)
32
+ request.body = body
33
+ send_request(request, uri)
34
+ end
35
+
36
+ def save_secret(secret)
37
+ secret_account = secret.to_account
38
+ secret_account.folder = session_adapter.selected_folder.id
39
+
40
+ persisted_secret = Account.find_by_name_and_folder_id(secret.name,
41
+ session_adapter.selected_folder.id)
42
+ if persisted_secret
43
+ patch("accounts/#{persisted_secret.id}", secret_account.to_json)
44
+ else
45
+ post('accounts', secret_account.to_json)
46
+ end
47
+ end
48
+
49
+ def find_account_by_name(name)
50
+ secret_account = Account.find_by_name_and_folder_id(name, session_adapter.selected_folder.id)
51
+
52
+ raise CryptopusAccountNotFoundError unless secret_account
53
+
54
+ secret_account
55
+ end
56
+
57
+ def renewed_auth_token
58
+ json = get("api_users/#{current_user_id}/token")
59
+ JSON.parse(json)['token']
60
+ end
61
+
62
+ private
63
+
64
+ def current_user_id
65
+ users = JSON.parse(get('api_users'), symbolize_names: true)
66
+ users[:data].find do |user|
67
+ user[:attributes][:username] == session_adapter.session_data[:username]
68
+ end[:id]
69
+ end
70
+
71
+ def session_adapter
72
+ @session_adapter ||= SessionAdapter.new
73
+ end
74
+
75
+ def header_token
76
+ Base64.strict_encode64(session_adapter.session_data[:token] || '')
77
+ end
78
+
79
+ def new_request(verb, uri)
80
+ request = Object.const_get("Net::HTTP::#{verb.capitalize}").new(uri)
81
+ request['Authorization-User'] = session_adapter.session_data[:username]
82
+ request['Authorization-Password'] = header_token
83
+ if [:post, :patch].include? verb
84
+ request['Content-Type'] = 'application/json'
85
+ request['Accept'] = 'application/vnd.api+json'
86
+ end
87
+ request
88
+ end
89
+
90
+ def send_request(request, uri)
91
+ is_ssl_connection = uri.port == 443
92
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: is_ssl_connection) do |http|
93
+ http.request(request)
94
+ end
95
+ raise UnauthorizedError if response.is_a?(Net::HTTPUnauthorized)
96
+ raise ForbiddenError if response.is_a?(Net::HTTPForbidden)
97
+
98
+ response.body
99
+ end
100
+ end