comply-cli 0.0.1

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: fcf980a7d9d589a6a1cfe1da71914a000bbe0449847e87ea3045eb0712322d8e
4
+ data.tar.gz: f2842dbe4f9e4e2d2396a3b8ac6c14a722c09d48d6ad7240b6ef8bcd73b1c5e6
5
+ SHA512:
6
+ metadata.gz: f78cb51dd7da44ad62c11d12e7465e06f8f4c3510be00851ce6e860647f4c33fa8c769adf70c6e18f39416f45b207e86b7b359c1a60386ffdb172da2841edd30
7
+ data.tar.gz: 2adb450ccc5370b987d933721d3b2c82dd734e1dfe71cbef8a4349cc06a5b800c6eca041516e38c3612558f00da603c12aff69735453aaeac0443af0848ff164
@@ -0,0 +1 @@
1
+ * @wongal5
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ *.dump
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ /.idea
20
+
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
@@ -0,0 +1,9 @@
1
+ sudo: false
2
+
3
+ rvm:
4
+ - "2.5"
5
+
6
+ script:
7
+ - bundle exec rake
8
+ - bundle exec script/sync-readme-usage
9
+ - git diff --exit-code
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in aptible-cli.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Aptible, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,54 @@
1
+ # Comply CLI
2
+
3
+ Command-line interface for Aptible Comply.
4
+
5
+ ## Installation
6
+
7
+ _Note: while this gem is in development, it is (a) private, and (b) built against API endpoints that are still in development. As a result, the instructions here are both temporary (subject to change) and more complicated than they will eventually be._
8
+
9
+ 1. Clone this repo: `git clone git@github.com:aptible/comply-cli.git`
10
+ 2. From the repo directory, install the gem:
11
+
12
+ pushd comply-cli/
13
+ bundle install
14
+ bundle exec rake install
15
+ popd
16
+
17
+ When using the gem, you will need to configure it to point at a version of the Comply CLI that supports the endpoints required by the CLI. At this moment, the "comply-api-cli" app in the "aptible-staging" environment on Deploy is the deployment kept mos up to date. To use this app with comply-cli, set `APTIBLE_AUTH_ROOT_URL` and `APTIBLE_COMPLY_ROOT_URL` each time you open a new shell (terminal) to use the CLI:
18
+
19
+ ```
20
+ export APTIBLE_AUTH_ROOT_URL=https://auth-api-master.aptible-staging.com APTIBLE_COMPLY_ROOT_URL=https://comply-api-cli.aptible-staging.com
21
+ ```
22
+
23
+
24
+ ## Usage
25
+
26
+ From `comply help`:
27
+
28
+ <!-- BEGIN USAGE -->
29
+ ```
30
+ Commands:
31
+ comply help [COMMAND] # Describe available commands or one specific command
32
+ comply integrations:enable INTEGRATION_ID # Enable an integration
33
+ comply integrations:list # List integrations
34
+ comply integrations:sync INTEGRATION_ID # Sync an integration
35
+ comply integrations:update INTEGRATION_ID # Enable an integration
36
+ comply login # Log in to Aptible
37
+ comply programs:select # Select a program for CLI context
38
+ comply version # Print Aptible CLI version
39
+ ```
40
+ <!-- END USAGE -->
41
+
42
+ ## Contributing
43
+
44
+ 1. Fork the project.
45
+ 1. Commit your changes, with specs.
46
+ 1. Ensure that your code passes specs (`rake spec`) and meets Aptible's Ruby style guide (`rake rubocop`).
47
+ 1. If you add a command, sync this README (`bundle exec script/sync-readme-usage`).
48
+ 1. Create a new pull request on GitHub.
49
+
50
+ ## Copyright and License
51
+
52
+ MIT License, see [LICENSE](LICENSE.md) for details.
53
+
54
+ Copyright (c) 2019 [Aptible](https://www.aptible.com) and contributors.
@@ -0,0 +1,4 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'aptible/tasks'
4
+ Aptible::Tasks.load_tasks
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'comply/cli'
5
+ rescue
6
+ require 'rubygems'
7
+ require 'comply/cli'
8
+ end
9
+
10
+ begin
11
+ Comply::CLI::Agent.start
12
+ rescue HyperResource::ClientError => e
13
+ m = if e.body['error'] == 'invalid_token'
14
+ 'API authentication error: please run comply login'
15
+ else
16
+ "An error occurred: #{e.body['message']}"
17
+ end
18
+ puts m
19
+ exit 1
20
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'English'
6
+ require 'comply/cli/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'comply-cli'
10
+ spec.version = Comply::CLI::VERSION
11
+ spec.authors = ['Frank Macreery']
12
+ spec.email = ['frank@macreery.com']
13
+ spec.description = 'Comply CLI'
14
+ spec.summary = 'Command-line interface for Aptible Comply'
15
+ spec.homepage = 'https://github.com/aptible/comply-cli'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files`.split($RS)
19
+ spec.executables = spec.files.grep(%r{bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{spec/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'aptible-resource', '~> 1.1'
24
+ spec.add_dependency 'aptible-auth', '~> 1.0'
25
+ spec.add_dependency 'aptible-comply', '>= 0.1.0'
26
+ spec.add_dependency 'thor', '~> 0.20.0'
27
+ spec.add_dependency 'chronic_duration', '~> 0.10.6'
28
+ spec.add_dependency 'highline', '~> 1.7'
29
+ spec.add_development_dependency 'rspec', '~> 3.2'
30
+ spec.add_development_dependency 'fabrication', '~> 2.15.2'
31
+ spec.add_development_dependency 'climate_control', '= 0.0.3'
32
+ spec.add_development_dependency 'aptible-tasks', '~> 0.5.8'
33
+ spec.add_development_dependency 'pry'
34
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'ext/string'
2
+
3
+ require 'comply/cli/version'
4
+ require 'comply/cli/agent'
5
+
6
+ module Comply
7
+ module CLI
8
+ end
9
+ end
@@ -0,0 +1,122 @@
1
+ require 'aptible/auth'
2
+ require 'thor'
3
+ require 'chronic_duration'
4
+ require 'highline/import'
5
+
6
+ Dir[File.join(__dir__, 'helpers', '*.rb')].each { |file| require file }
7
+ Dir[File.join(__dir__, 'subcommands', '*.rb')].each { |file| require file }
8
+
9
+ module Comply
10
+ module CLI
11
+ class Agent < Thor
12
+ include Thor::Actions
13
+
14
+ include Helpers::Token
15
+ include Helpers::Program
16
+
17
+ include Subcommands::Integrations
18
+
19
+ # Forward return codes on failures.
20
+ def self.exit_on_failure?
21
+ true
22
+ end
23
+
24
+ def initialize(*)
25
+ Aptible::Resource.configure { |conf| conf.user_agent = version_string }
26
+ super
27
+ end
28
+
29
+ desc 'version', 'Print Aptible CLI version'
30
+ def version
31
+ puts version_string
32
+ end
33
+
34
+ desc 'login', 'Log in to Aptible'
35
+ option :email
36
+ option :password
37
+ option :lifetime, desc: 'The duration the token should be valid for ' \
38
+ '(example usage: 24h, 1d, 600s, etc.)'
39
+ option :otp_token, desc: 'A token generated by your second-factor app'
40
+ def login
41
+ email = options[:email] || ask('Email: ')
42
+ password = options[:password] || ask('Password: ', echo: false)
43
+ puts ''
44
+
45
+ token_options = { email: email, password: password }
46
+
47
+ otp_token = options[:otp_token]
48
+ token_options[:otp_token] = otp_token if otp_token
49
+
50
+ begin
51
+ lifetime = '1w'
52
+ lifetime = '12h' if token_options[:otp_token]
53
+ lifetime = options[:lifetime] if options[:lifetime]
54
+
55
+ duration = ChronicDuration.parse(lifetime)
56
+ if duration.nil?
57
+ raise Thor::Error, "Invalid token lifetime requested: #{lifetime}"
58
+ end
59
+
60
+ token_options[:expires_in] = duration
61
+ token = Aptible::Auth::Token.create(token_options)
62
+ rescue OAuth2::Error => e
63
+ if e.code == 'otp_token_required'
64
+ token_options[:otp_token] = options[:otp_token] ||
65
+ ask('2FA Token: ')
66
+ retry
67
+ end
68
+
69
+ raise Thor::Error, 'Could not authenticate with given credentials: ' \
70
+ "#{e.code}"
71
+ end
72
+
73
+ save_token(token.access_token)
74
+ puts "Token written to #{token_file}"
75
+
76
+ lifetime_format = { units: 2, joiner: ', ' }
77
+ token_lifetime = (token.expires_at - token.created_at).round
78
+ expires_in = ChronicDuration.output(token_lifetime, lifetime_format)
79
+ puts "This token will expire after #{expires_in} " \
80
+ '(use --lifetime to customize)'
81
+
82
+ # Select a default program
83
+ set_default_program
84
+ end
85
+
86
+ desc 'programs:select', 'Select a program for CLI context'
87
+ define_method 'programs:select' do
88
+ candidates = accessible_programs
89
+ current_program_id = begin
90
+ fetch_program_id
91
+ rescue Thor::Error
92
+ nil
93
+ end
94
+
95
+ choose do |menu|
96
+ menu.prompt = 'Choose a program (* is current default):'
97
+ candidates.each do |program|
98
+ pretty = pretty_print_program(program)
99
+ if program.id == current_program_id
100
+ pretty = "* #{pretty}"
101
+ menu.default = pretty
102
+ end
103
+
104
+ menu.choice pretty do
105
+ save_program_id program.id
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def version_string
114
+ bits = [
115
+ 'comply-cli',
116
+ "v#{Comply::CLI::VERSION}"
117
+ ]
118
+ bits.join ' '
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,55 @@
1
+ require 'aptible/comply'
2
+ require 'json'
3
+
4
+ module Comply
5
+ module CLI
6
+ module Helpers
7
+ module Integration
8
+ def prettify_integration(integration)
9
+ integration.integration_type
10
+ end
11
+
12
+ def pretty_print_integration(integration)
13
+ "#{prettify_integration(integration)} (#{integration.id})"
14
+ end
15
+
16
+ def prompt_and_create_integration(type)
17
+ env = prompt_for_env(type)
18
+ default_program.create_integration(integration_type: type, env: env)
19
+ end
20
+
21
+ def prompt_and_update_integration(integration)
22
+ env = prompt_for_env(integration.integration_type)
23
+ integration.update(env: env)
24
+ end
25
+
26
+ def prompt_for_env(type)
27
+ env = {}
28
+ env_vars_by_prompt(type).each do |human, key|
29
+ env[key] = ask("#{human}:", echo: false)
30
+ puts
31
+ end
32
+
33
+ env
34
+ end
35
+
36
+ def env_vars_by_prompt(type)
37
+ case type
38
+ when 'gsuite'
39
+ {
40
+ 'Client ID' => 'GOOGLE_CLIENT_ID',
41
+ 'Client Secret' => 'GOOGLE_CLIENT_SECRET',
42
+ 'Access Token' => 'GOOGLE_ACCESS_TOKEN',
43
+ 'Refresh Token' => 'GOOGLE_REFRESH_TOKEN'
44
+ }
45
+ when 'okta'
46
+ {
47
+ 'Org Name' => 'OKTA_ORG_NAME',
48
+ 'API Key' => 'OKTA_API_KEY'
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,81 @@
1
+ require 'aptible/comply'
2
+ require 'json'
3
+
4
+ require_relative 'token'
5
+
6
+ module Comply
7
+ module CLI
8
+ module Helpers
9
+ module Program
10
+ include Helpers::Token
11
+
12
+ PROGRAM_ENV_VAR = 'APTIBLE_PROGRAM_ID'.freeze
13
+
14
+ def default_program
15
+ return nil unless (id = fetch_program_id)
16
+ @default_program ||= Aptible::Comply::Program.find(
17
+ id, token: fetch_token
18
+ )
19
+ end
20
+
21
+ def set_default_program
22
+ default_program = accessible_programs.first
23
+ save_program_id(default_program.id) if default_program
24
+ end
25
+
26
+ def pretty_print_program(program)
27
+ "#{program.organization.name} (#{program.id})"
28
+ end
29
+
30
+ def fetch_program_id
31
+ @program_id ||=
32
+ ENV[PROGRAM_ENV_VAR] ||
33
+ current_program_id_hash[Aptible::Comply.configuration.root_url]
34
+ return @program_id if @program_id
35
+ raise Thor::Error, 'Could not read program: please run comply ' \
36
+ "programs:select or set #{PROGRAM_ENV_VAR}"
37
+ end
38
+
39
+ def save_program_id(program_id)
40
+ hash = current_program_id_hash.merge(
41
+ Aptible::Comply.configuration.root_url => program_id
42
+ )
43
+
44
+ FileUtils.mkdir_p(File.dirname(program_id_file))
45
+
46
+ File.open(program_id_file, 'w', 0o600) do |file|
47
+ file.puts hash.to_json
48
+ end
49
+ rescue StandardError => e
50
+ m = "Could not write program to #{program_id_file}: #{e}. " \
51
+ 'Check filesystem permissions.'
52
+ raise Thor::Error, m
53
+ end
54
+
55
+ def current_program_id_hash
56
+ JSON.parse(File.read(program_id_file))
57
+ rescue
58
+ {}
59
+ end
60
+
61
+ def program_id_file
62
+ File.join ENV['HOME'], '.aptible', 'programs.json'
63
+ end
64
+
65
+ def accessible_programs
66
+ # If a user is a member of a role in ACCOUNT_MANAGEMENT_ROLE_IDS
67
+ # in Comply, they have read access to ALL programs. As a result,
68
+ # when offering programs for customers to access, we select just
69
+ # those which actually belong to their organization(s).
70
+
71
+ programs = Aptible::Comply::Program.all(token: fetch_token)
72
+ orgs = Aptible::Auth::Organization.all(token: fetch_token)
73
+
74
+ programs.select do |program|
75
+ orgs.map(&:href).include?(program.organization_url)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,54 @@
1
+ require 'aptible/auth'
2
+ require 'json'
3
+
4
+ module Comply
5
+ module CLI
6
+ module Helpers
7
+ module Token
8
+ TOKEN_ENV_VAR = 'APTIBLE_ACCESS_TOKEN'.freeze
9
+
10
+ def current_user_email
11
+ @current_user_email ||=
12
+ Aptible::Auth::Agent.new(token: fetch_token).me.email
13
+ end
14
+
15
+ def fetch_token
16
+ @token ||= ENV[TOKEN_ENV_VAR] ||
17
+ current_token_hash[Aptible::Auth.configuration.root_url]
18
+ return @token if @token
19
+ raise Thor::Error, 'Could not read token: please run comply login ' \
20
+ "or set #{TOKEN_ENV_VAR}"
21
+ end
22
+
23
+ def save_token(token)
24
+ hash = current_token_hash.merge(
25
+ Aptible::Auth.configuration.root_url => token
26
+ )
27
+
28
+ FileUtils.mkdir_p(File.dirname(token_file))
29
+
30
+ File.open(token_file, 'w', 0o600) do |file|
31
+ file.puts hash.to_json
32
+ end
33
+ rescue StandardError => e
34
+ m = "Could not write token to #{token_file}: #{e}. " \
35
+ 'Check filesystem permissions.'
36
+ raise Thor::Error, m
37
+ end
38
+
39
+ def current_token_hash
40
+ # NOTE: older versions of the CLI did not properly create the
41
+ # token_file with mode 600, which is why we update it when reading.
42
+ File.chmod(0o600, token_file)
43
+ JSON.parse(File.read(token_file))
44
+ rescue
45
+ {}
46
+ end
47
+
48
+ def token_file
49
+ File.join ENV['HOME'], '.aptible', 'tokens.json'
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ module Comply
2
+ module CLI
3
+ module Subcommands
4
+ module Integrations
5
+ include Helpers::Integration
6
+
7
+ def self.included(thor)
8
+ thor.class_eval do
9
+ desc 'integrations:list', 'List integrations'
10
+ define_method 'integrations:list' do
11
+ integrations = default_program.integrations
12
+ if integrations.empty?
13
+ say 'No integrations found.'
14
+ else
15
+ integrations.each do |integration|
16
+ say pretty_print_integration(integration)
17
+ end
18
+ end
19
+ end
20
+
21
+ desc 'integrations:enable INTEGRATION_ID', 'Enable an integration'
22
+ define_method 'integrations:enable' do |integration_type|
23
+ integration = default_program.integrations.find do |i|
24
+ i.integration_type == integration_type
25
+ end
26
+ raise Thor::Error, 'Integration already enabled' if integration
27
+
28
+ prompt_and_create_integration(integration_type)
29
+ end
30
+
31
+ desc 'integrations:update INTEGRATION_ID', 'Enable an integration'
32
+ define_method 'integrations:update' do |integration_type|
33
+ integration = default_program.integrations.find do |i|
34
+ i.integration_type == integration_type
35
+ end
36
+ raise Thor::Error, 'Integration not found' unless integration
37
+
38
+ prompt_and_update_integration(integration)
39
+ end
40
+
41
+ desc 'integrations:sync INTEGRATION_ID', 'Sync an integration'
42
+ define_method 'integrations:sync' do |integration_type|
43
+ integration = default_program.integrations.find do |i|
44
+ i.integration_type == integration_type
45
+ end
46
+ raise Thor::Error, 'Integration not found' unless integration
47
+
48
+ integration.links['sync'].post
49
+ say 'Integration synced'
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ module Comply
2
+ module CLI
3
+ VERSION = '0.0.1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def uuid?
3
+ !!(self =~ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require 'open3'
3
+
4
+ USAGE = ARGV.fetch(0, 'README.md')
5
+
6
+ puts "Sync CLI usage in #{USAGE}"
7
+
8
+ txt, err, status = Open3.capture3(
9
+ { 'THOR_COLUMNS' => '1000' },
10
+ 'bundle', 'exec', 'bin/comply', 'help'
11
+ )
12
+
13
+ raise "Failed to extract usage: #{err}" unless status.success?
14
+
15
+ usage = "```\n#{txt.gsub(/^$\n/, '')}```\n"
16
+
17
+ bits = []
18
+
19
+ File.open(USAGE) do |f|
20
+ in_usage = false
21
+
22
+ f.each_line do |l|
23
+ in_usage = false if l.include?('END USAGE')
24
+
25
+ bits << l unless in_usage
26
+
27
+ if l.include?('BEGIN USAGE')
28
+ in_usage = true
29
+ bits << usage
30
+ end
31
+ end
32
+ end
33
+
34
+ File.write(USAGE, bits.join(''))
@@ -0,0 +1,196 @@
1
+ require 'spec_helper'
2
+
3
+ describe Comply::CLI::Agent do
4
+ before do
5
+ allow(subject).to receive(:ask)
6
+ allow(subject).to receive(:save_token)
7
+ allow(subject).to receive(:token_file).and_return 'some.json'
8
+ end
9
+
10
+ describe 'version' do
11
+ it 'should print the version' do
12
+ version = Comply::CLI::VERSION
13
+
14
+ expect do
15
+ subject.version
16
+ end.to output("comply-cli v#{version}\n").to_stdout
17
+ end
18
+ end
19
+
20
+ describe 'login' do
21
+ let(:token) { double('Aptible::Auth::Token') }
22
+ let(:created_at) { Time.now }
23
+ let(:expires_at) { created_at + 1.week }
24
+
25
+ before do
26
+ m = -> (code) { @code = code }
27
+ OAuth2::Error.send :define_method, :initialize, m
28
+
29
+ allow(token).to receive(:access_token).and_return 'access_token'
30
+ allow(token).to receive(:created_at).and_return created_at
31
+ allow(token).to receive(:expires_at).and_return expires_at
32
+ allow(subject).to receive(:puts) {}
33
+
34
+ allow(subject).to receive(:set_default_program) {}
35
+ end
36
+
37
+ it 'should save a token to ~/.aptible/tokens' do
38
+ allow(Aptible::Auth::Token).to receive(:create).and_return token
39
+ expect(subject).to receive(:save_token).with('access_token')
40
+ subject.login
41
+ end
42
+
43
+ it 'should output the token location and token lifetime' do
44
+ allow(Aptible::Auth::Token).to receive(:create).and_return token
45
+ allow(subject).to receive(:puts).and_call_original
46
+
47
+ expect { subject.login }.to output(/token written to.*json/i).to_stdout
48
+ expect { subject.login }.to output(/expire after 7 days/i).to_stdout
49
+ end
50
+
51
+ it 'should raise an error if authentication fails' do
52
+ allow(Aptible::Auth::Token).to receive(:create)
53
+ .and_raise(OAuth2::Error, 'foo')
54
+ expect do
55
+ subject.login
56
+ end.to raise_error 'Could not authenticate with given credentials: foo'
57
+ end
58
+
59
+ it 'should use command line arguments if passed' do
60
+ options = { email: 'test@example.com', password: 'password',
61
+ lifetime: '30 minutes' }
62
+ allow(subject).to receive(:options).and_return options
63
+ args = { email: options[:email], password: options[:password],
64
+ expires_in: 30.minutes.seconds }
65
+ expect(Aptible::Auth::Token).to receive(:create).with(args) { token }
66
+ subject.login
67
+ end
68
+
69
+ it 'should default to 1 week expiry when OTP is disabled' do
70
+ options = { email: 'test@example.com', password: 'password' }
71
+ allow(subject).to receive(:options).and_return options
72
+ args = options.dup.merge(expires_in: 1.week.seconds)
73
+ expect(Aptible::Auth::Token).to receive(:create).with(args) { token }
74
+ subject.login
75
+ end
76
+
77
+ it 'should fail if the lifetime is invalid' do
78
+ options = { email: 'test@example.com', password: 'password',
79
+ lifetime: 'this is sparta' }
80
+ allow(subject).to receive(:options).and_return options
81
+
82
+ expect { subject.login }.to raise_error(/Invalid token lifetime/)
83
+ end
84
+
85
+ it 'should set a default program' do
86
+ allow(Aptible::Auth::Token).to receive(:create).and_return token
87
+ expect(subject).to receive(:set_default_program)
88
+ subject.login
89
+ end
90
+
91
+ context 'with OTP' do
92
+ let(:email) { 'foo@example.org' }
93
+ let(:password) { 'bar' }
94
+ let(:token) { '123456' }
95
+
96
+ context 'with options' do
97
+ before do
98
+ allow(subject).to receive(:options)
99
+ .and_return(email: email, password: password, otp_token: token)
100
+ end
101
+
102
+ it 'should authenticate without otp_token_required feedback' do
103
+ expect(Aptible::Auth::Token).to receive(:create)
104
+ .with(email: email, password: password, otp_token: token,
105
+ expires_in: 12.hours.seconds)
106
+ .once
107
+ .and_return(token)
108
+
109
+ subject.login
110
+ end
111
+ end
112
+
113
+ context 'with prompts' do
114
+ before do
115
+ [
116
+ [['Email: '], email],
117
+ [['Password: ', echo: false], password],
118
+ [['2FA Token: '], token]
119
+ ].each do |prompt, val|
120
+ expect(subject).to receive(:ask).with(*prompt).once.and_return(val)
121
+ end
122
+ end
123
+
124
+ it 'should prompt for an OTP token and use it' do
125
+ expect(Aptible::Auth::Token).to receive(:create)
126
+ .with(email: email, password: password, expires_in: 1.week.seconds)
127
+ .once
128
+ .and_raise(OAuth2::Error, 'otp_token_required')
129
+
130
+ expect(Aptible::Auth::Token).to receive(:create)
131
+ .with(email: email, password: password, otp_token: token,
132
+ expires_in: 12.hours.seconds)
133
+ .once
134
+ .and_return(token)
135
+
136
+ subject.login
137
+ end
138
+
139
+ it 'should let the user override the default lifetime' do
140
+ expect(Aptible::Auth::Token).to receive(:create)
141
+ .with(email: email, password: password, expires_in: 1.day.seconds)
142
+ .once
143
+ .and_raise(OAuth2::Error, 'otp_token_required')
144
+
145
+ expect(Aptible::Auth::Token).to receive(:create)
146
+ .with(email: email, password: password, otp_token: token,
147
+ expires_in: 1.day.seconds)
148
+ .once
149
+ .and_return(token)
150
+
151
+ allow(subject).to receive(:options).and_return(lifetime: '1d')
152
+ subject.login
153
+ end
154
+
155
+ it 'should not retry non-OTP errors.' do
156
+ expect(Aptible::Auth::Token).to receive(:create)
157
+ .with(email: email, password: password, expires_in: 1.week.seconds)
158
+ .once
159
+ .and_raise(OAuth2::Error, 'otp_token_required')
160
+
161
+ expect(Aptible::Auth::Token).to receive(:create)
162
+ .with(email: email, password: password, otp_token: token,
163
+ expires_in: 12.hours.seconds)
164
+ .once
165
+ .and_raise(OAuth2::Error, 'foo')
166
+
167
+ expect { subject.login }.to raise_error(/Could not authenticate/)
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ describe 'programs:select' do
174
+ # TODO: Try to get this working
175
+ # https://github.com/JEG2/highline/issues/176 might be helpful
176
+
177
+ let(:p1) { Fabricate(:program) }
178
+ let(:p2) { Fabricate(:program) }
179
+
180
+ before do
181
+ allow(subject).to receive(:accessible_programs) { [p1, p2] }
182
+ allow(subject).to receive(:fetch_program_id) { p1.id }
183
+ allow_any_instance_of(HighLine).to receive(:get_line) { '1' }
184
+ end
185
+
186
+ skip 'displays a selection of available programs' do
187
+ lines = ["* #{subject.pretty_print_program(p1)}",
188
+ subject.pretty_print_program(p2),
189
+ 'Choose a program']
190
+
191
+ lines.each do |line|
192
+ expect { subject.send('programs:select') }.to output(line).to_stdout
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ describe Comply::CLI::Helpers::Program do
5
+ around do |example|
6
+ Dir.mktmpdir { |home| ClimateControl.modify(HOME: home) { example.run } }
7
+ end
8
+
9
+ subject { Class.new.send(:include, described_class).new }
10
+
11
+ let(:program_id) { SecureRandom.uuid }
12
+ let(:token) { double('token') }
13
+
14
+ before do
15
+ allow(subject).to receive(:fetch_token) { token }
16
+ end
17
+
18
+ describe '#save_program / #fetch_program' do
19
+ it 'reads back a program ID it saved' do
20
+ subject.save_program_id(program_id)
21
+ expect(subject.fetch_program_id).to eq(program_id)
22
+ end
23
+ end
24
+
25
+ describe 'accessible_programs' do
26
+ it 'filters programs' do
27
+ o1 = Fabricate(:organization)
28
+ o2 = Fabricate(:organization)
29
+
30
+ p1 = Fabricate(:program, organization: o1)
31
+ p2 = Fabricate(:program, organization: o2)
32
+ p3 = Fabricate(:program)
33
+
34
+ allow(Aptible::Comply::Program).to receive(:all) { [p1, p2, p3] }
35
+ allow(Aptible::Auth::Organization).to receive(:all) { [o1, o2] }
36
+
37
+ programs = subject.accessible_programs
38
+ expect(programs.map(&:id).sort).to eq [p1.id, p2.id].sort
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Comply::CLI::Helpers::Token do
4
+ around do |example|
5
+ Dir.mktmpdir { |home| ClimateControl.modify(HOME: home) { example.run } }
6
+ end
7
+
8
+ subject { Class.new.send(:include, described_class).new }
9
+
10
+ describe '#save_token / #fetch_token' do
11
+ it 'reads back a token it saved' do
12
+ subject.save_token('foo')
13
+ expect(subject.fetch_token).to eq('foo')
14
+ end
15
+ end
16
+
17
+ context 'permissions' do
18
+ before { skip 'Windows' if Gem.win_platform? }
19
+
20
+ describe '#save_token' do
21
+ it 'creates the token_file with mode 600' do
22
+ subject.save_token('foo')
23
+ expect(format('%o', File.stat(subject.token_file).mode))
24
+ .to eq('100600')
25
+ end
26
+ end
27
+
28
+ describe '#current_token_hash' do
29
+ it 'updates the token_file to mode 600' do
30
+ subject.save_token('foo')
31
+ File.chmod(0o644, subject.token_file)
32
+ expect(format('%o', File.stat(subject.token_file).mode))
33
+ .to eq('100644')
34
+
35
+ subject.current_token_hash
36
+ expect(format('%o', File.stat(subject.token_file).mode))
37
+ .to eq('100600')
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Comply::CLI::Agent do
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Comply::CLI::Agent do
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Comply::CLI do
4
+ end
@@ -0,0 +1,8 @@
1
+ class StubOrganization < OpenStruct
2
+ end
3
+
4
+ Fabricator(:organization, from: :stub_organization) do
5
+ id { Fabricate.sequence(:organization_id) { |i| i } }
6
+
7
+ href { |attrs| "https://auth.aptible.com/organizations/#{attrs[:id]}" }
8
+ end
@@ -0,0 +1,11 @@
1
+ class StubProgram < OpenStruct
2
+ end
3
+
4
+ Fabricator(:program, from: :stub_program) do
5
+ id { Fabricate.sequence(:program_id) { |i| i } }
6
+
7
+ organization
8
+ organization_url { |attrs| attrs[:organization].href }
9
+
10
+ errors { Aptible::Resource::Errors.new }
11
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ Bundler.require :development
5
+
6
+ # Load shared spec files
7
+ Dir["#{File.dirname(__FILE__)}/shared/**/*.rb"].each do |file|
8
+ require file
9
+ end
10
+
11
+ # Require library up front
12
+ require 'comply/cli'
metadata ADDED
@@ -0,0 +1,235 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: comply-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Frank Macreery
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aptible-resource
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aptible-auth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aptible-comply
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: thor
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.20.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.20.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: chronic_duration
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.10.6
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.10.6
83
+ - !ruby/object:Gem::Dependency
84
+ name: highline
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.7'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: fabrication
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 2.15.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 2.15.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: climate_control
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '='
130
+ - !ruby/object:Gem::Version
131
+ version: 0.0.3
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '='
137
+ - !ruby/object:Gem::Version
138
+ version: 0.0.3
139
+ - !ruby/object:Gem::Dependency
140
+ name: aptible-tasks
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.5.8
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.5.8
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: Comply CLI
168
+ email:
169
+ - frank@macreery.com
170
+ executables:
171
+ - comply
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".github/CODEOWNERS"
176
+ - ".gitignore"
177
+ - ".rspec"
178
+ - ".travis.yml"
179
+ - Gemfile
180
+ - LICENSE.md
181
+ - README.md
182
+ - Rakefile
183
+ - bin/comply
184
+ - comply-cli.gemspec
185
+ - lib/comply/cli.rb
186
+ - lib/comply/cli/agent.rb
187
+ - lib/comply/cli/helpers/integration.rb
188
+ - lib/comply/cli/helpers/program.rb
189
+ - lib/comply/cli/helpers/token.rb
190
+ - lib/comply/cli/subcommands/integrations.rb
191
+ - lib/comply/cli/version.rb
192
+ - lib/comply/ext/string.rb
193
+ - script/sync-readme-usage
194
+ - spec/comply/cli/agent_spec.rb
195
+ - spec/comply/cli/helpers/program_spec.rb
196
+ - spec/comply/cli/helpers/token_spec.rb
197
+ - spec/comply/cli/subcommands/groups_spec.rb
198
+ - spec/comply/cli/subcommands/integrations_spec.rb
199
+ - spec/comply/cli_spec.rb
200
+ - spec/fabricators/organization_fabricator.rb
201
+ - spec/fabricators/program_fabricator.rb
202
+ - spec/spec_helper.rb
203
+ homepage: https://github.com/aptible/comply-cli
204
+ licenses:
205
+ - MIT
206
+ metadata: {}
207
+ post_install_message:
208
+ rdoc_options: []
209
+ require_paths:
210
+ - lib
211
+ required_ruby_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ required_rubygems_version: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - ">="
219
+ - !ruby/object:Gem::Version
220
+ version: '0'
221
+ requirements: []
222
+ rubygems_version: 3.0.3
223
+ signing_key:
224
+ specification_version: 4
225
+ summary: Command-line interface for Aptible Comply
226
+ test_files:
227
+ - spec/comply/cli/agent_spec.rb
228
+ - spec/comply/cli/helpers/program_spec.rb
229
+ - spec/comply/cli/helpers/token_spec.rb
230
+ - spec/comply/cli/subcommands/groups_spec.rb
231
+ - spec/comply/cli/subcommands/integrations_spec.rb
232
+ - spec/comply/cli_spec.rb
233
+ - spec/fabricators/organization_fabricator.rb
234
+ - spec/fabricators/program_fabricator.rb
235
+ - spec/spec_helper.rb