comply-cli 0.0.1

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