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.
- checksums.yaml +7 -0
- data/.rubocop.yml +121 -0
- data/.travis.yml +9 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +75 -0
- data/README.md +63 -0
- data/bin/cry +6 -0
- data/ccli.gemspec +24 -0
- data/lib/adapters/cluster_secret_adapter.rb +70 -0
- data/lib/adapters/cryptopus_adapter.rb +100 -0
- data/lib/adapters/k8s_adapter.rb +20 -0
- data/lib/adapters/ose_adapter.rb +20 -0
- data/lib/adapters/session_adapter.rb +74 -0
- data/lib/cli.rb +342 -0
- data/lib/errors.rb +40 -0
- data/lib/models/account.rb +44 -0
- data/lib/models/folder.rb +25 -0
- data/lib/models/k8s_secret.rb +11 -0
- data/lib/models/ose_secret.rb +28 -0
- data/lib/models/team.rb +45 -0
- data/lib/presenters/team_presenter.rb +18 -0
- data/lib/serializers/account_serializer.rb +57 -0
- data/lib/serializers/folder_serializer.rb +12 -0
- data/lib/serializers/ose_secret_serializer.rb +16 -0
- data/lib/serializers/team_serializer.rb +18 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -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
|
data/.rubocop.yml
ADDED
@@ -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
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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
data/ccli.gemspec
ADDED
@@ -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
|