ccli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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