Skip to content

Add Chef Automate support to inspec compliance login #2203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 26, 2017

Conversation

jerryaldrichiii
Copy link
Contributor

@jerryaldrichiii jerryaldrichiii commented Oct 2, 2017

This provides a single interface for logging into either Chef Automate or Chef Compliance servers. Server type is evaluated at run time via HTTP responses from designated endpoints.

This also moves the login logic from Compliance::ComplianceCLI to a separate set of modules in Compliance::API. Thus removing logic from Thor and allowing for more in depth Unit testing.

This also adds deprecation warnings for login_automate and --usertoken.

This fixes #1795.

Signed-off-by: Jerry Aldrich jerryaldrichiii@gmail.com

This provides a single interface for logging into either Chef Automate
or Chef Compliance servers. Server type is evaluated at run time via
HTTP responses from designated endpoints.

This also moves the login logic from `Compliance::ComplianceCLI` to a
separate set of modules in `Compliance::API`. This removes logic from
Thor and allows for more in depth Unit testing.

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
@jerryaldrichiii jerryaldrichiii requested a review from a team as a code owner October 2, 2017 22:01
Copy link
Contributor

@adamleff adamleff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just flipped through this quickly, some comments and questions, probably worth a Zoom review when it's no longer WIP.


# API Implementation does not hold any state by itself,
# everything will be stored in local Configuration store
class API # rubocop:disable Metrics/ClassLength

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra newline at class beginning

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pick all the nits! Fixed.

@@ -238,5 +243,11 @@ def self.server_version_from_config(config)
return nil unless config['version'].is_a?(Hash)
config['version']['version']
end

def self.determine_server_type(url, insecure)
return 'automate' if Compliance::HTTP.get(url + '/compliance/version', nil, insecure).code == '401'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more idiomatic to return symbols for something like this rather than strings.

return :automate if ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always prefer going the idiomatic route. Fixed. Thanks @adamleff!

def self.determine_server_type(url, insecure)
return 'automate' if Compliance::HTTP.get(url + '/compliance/version', nil, insecure).code == '401'
return 'compliance' if Compliance::HTTP.get(url + '/api/version', nil, insecure).code == '200'
raise CannotDetermineServerType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also add an exception message when raising.

raise CannotDetermineServerType, "Unable to determine if #{url} is a Compliance or Automate server"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixed.


options['url'] = options['server'] + '/compliance'
token = options['dctoken'] || options['token']
puts store_access_token(options, token)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's a WIP, to just a reminder to remove this :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store_access_token returns a string at this time. I will look at the flow and see if we should pass this up as a return from login.

Will investigate.

def login(options)
options['server_type'] = Compliance::API.determine_server_type(options['server'], options['insecure'])

return Login::ComplianceServer.login(options) if options['server_type'] == 'compliance'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you be doing the Automate login here too? Probably worth Zooming to review the expected flow, etc. would be good when you're ready :) This mid-method return with no comments as to why is confusing me as to your intentions for this method and how Automate login will be handled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This redirects to the login method of the ComplianceServer module if the server type is a Compliance server. I was returning here, since this will complete the login. Initially I had it just exit 0 at the end, but that was causing me issues with the Unit tests.

Will investigate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd second @adamleff here - If you're switching between two sets of logic, and you've encapsulated one in a module, I'd expect the other logic - for Automate login - to also be encapsulated in a module (presumably Login::AutomateServer).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I thought only to encapsulate the Compliance server stuff because it would be deprecated in the future leaving only Login...but the future is not now. I see your point.

Fixed.

Copy link
Contributor Author

@jerryaldrichiii jerryaldrichiii Oct 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamleff let me know what you think of the flow now that I've modified the namespacing.

private

def store_access_token(options, token)
token_info = { type: 'dctoken', msg: 'data colletor token' } if options['dctoken']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you ever not have either a dctoken or a token? What happens if token_info is not set?

I'd recommend changing it to something like:

token_info = if options['dctoken']
               { # dctoken stuff }
             else
               { # non-dctoken stuff }
             end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Fixed.

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
@jerryaldrichiii
Copy link
Contributor Author

Agreed, a Zoom review would be helpful! Thanks for the initial feedback @adamleff.

@clintoncwolfe
Copy link
Contributor

We should also update the README docs to reflect that the inspec compliance login_automate command is no longer present. See the README in the bundle. You've already removed it from the Thor-based CLI help, awesome!

option :insecure, aliases: :k, type: :boolean,
desc: 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
def login_automate(server) # rubocop:disable Metrics/AbcSize
def login(server) # rubocop:disable Metrics/AbcSize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need the rubocop disable flag here anymore (when you refactored this out, the method became much simpler). You can run a rubocop scan with
bundle exec rake lint

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great find! Fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, nice catch! Fixed.


# API Implementation does not hold any state by itself,
# everything will be stored in local Configuration store
class API # rubocop:disable Metrics/ClassLength
extend Login
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be a matter of taste, but I'd usually have the module name here more deeply namespaced - like Compliance::API::Login . I don't know that we'd ever (within InSpec) have a name collision with a module named Login, but being specific avoids that possibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! Fixed.

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
@jerryaldrichiii jerryaldrichiii force-pushed the ja/merge-login-commands branch from 7627db0 to f7d36f1 Compare October 4, 2017 16:19
@jerryaldrichiii
Copy link
Contributor Author

@clintoncwolfe I was certain I did a grep -R login_automate at one point and removed all references! Must have stashed and forgot about it somewhere.

Great catch! README.md in bundle is updated. I also removed references to --usertoken in all the places.

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
@jerryaldrichiii jerryaldrichiii changed the title WIP: Merge login and login_automate commands Merge login and login_automate commands Oct 4, 2017
@jerryaldrichiii jerryaldrichiii changed the base branch from release-2.0 to master October 4, 2017 20:40
@jerryaldrichiii jerryaldrichiii changed the title Merge login and login_automate commands Add Chef Automate support to inspec compliance login Oct 4, 2017
@jerryaldrichiii
Copy link
Contributor Author

Changing this PR to target master. This will not remove support for login_automate or --usertoken. Instead it will provide a deprecation warning. We will remove both in InSpec 2.0.

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
@jerryaldrichiii jerryaldrichiii force-pushed the ja/merge-login-commands branch from f1f9c49 to 5448731 Compare October 4, 2017 20:55
@adamleff adamleff added the Type: Enhancement Improves an existing feature label Oct 5, 2017
Copy link
Contributor

@adamleff adamleff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in great shape, @jerryaldrichiii. I'd like to see a bit of cleanup around where we raise exceptions and keeping method purposes focused. Let me know if any of my feedback is unclear.

class API
module Login
def login(options)
options['server_type'] = Compliance::API.determine_server_type(options['server'], options['insecure']).to_s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coercing to a string here seems... weird? We don't need it to be a string here or most of these methods -- only when we store the config to disk. Can we move the coercion there?

def self.determine_server_type(url, insecure)
return :automate if Compliance::HTTP.get(url + '/compliance/version', nil, insecure).code == '401'
return :compliance if Compliance::HTTP.get(url + '/api/version', nil, insecure).code == '200'
raise CannotDetermineServerType, "Unable to determine if #{url} is a Chef Automate or Chef Compliance server"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tossing out a thought here... I prefer to have exceptions raised at the point of the implementation where "it matters" (for lack of a better phrase). I'd almost prefer to see this method return nil if it can't figure out the server type, and let the caller determine how to handle. That way other methods can use this if needed without having to worry about the stack unwinding on them.

def self.determine_server_type(url, insecure)
  if Compliance::HTTP.get(url + '/compliance/version', nil, insecure).code == '401'
    :automate
  elsif Compliance::HTTP.get(url + '/api/version', nil, insecure).code == '200'
    :compliance
  end
end

def login(options)
options['server_type'] = Compliance::API.determine_server_type(options['server'], options['insecure']).to_s

return Login::ComplianceServer.login(options) if options['server_type'] == 'compliance'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is where I'd raise an exception if we don't know the server type since this is actually where an error should be raised to the user:

if options['server_type'] == :compliance
  Login::ComplianceServer.login(options)
elsif options['server_type'] == :automate
  Login::AutomateServer.login(options)
else
  raise CannotDetermineServerType, # .....
end


options['url'] = options['server'] + '/compliance'
token = options['dctoken'] || options['token']
puts store_access_token(options, token)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize I'm being a bit pedantic here, but it feels strange to me to have a method called store_access_token return a string intended to be user-facing messaging.

I'd rather see store_access_token return the Compliance::Configuration object it generated to successfully write the config to disk, and then a method that takes that config object and formats a message to the user. It will keep the concerns of the methods succinct and clear. This honestly just reads like a debug message to me. :)

config = store_access_token(options, token)

# print out a success message to the user
puts format_status_message(config)


raise msg unless success

puts msg
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same feedback as above - I bet there's a good opportunity for reuse here, actually.

msg = login_automate_config(url, options['user'], options['dctoken'], options['usertoken'], options['ent'], options['insecure'])
puts '', msg
exit 0
if options['usertoken']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite what was in the issue that preceded this PR, we probably don't need this deprecation here since the whole command is deprecated. As long as the inspec compliance login command doesn't accept a --usertoken flag, we're covered.

This does the following:
  - Moves `CannotDetermineServerType` error to `.login`
  - Changes methods that store configuration to return the configuration
  - Moves user output to one location in `.login`
  - Makes other small improvements

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
@jerryaldrichiii
Copy link
Contributor Author

I made all the changes you suggested @adamleff. Feel free to give it another look! 😄

Copy link
Contributor

@adamleff adamleff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lookin' good, @jerryaldrichiii - thanks!

Copy link
Contributor

@chris-rock chris-rock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jerryaldrichiii Awesome work. I just have some mini-questions before we can go forward

@@ -5,13 +5,16 @@
require 'net/http'
require 'uri'

require_relative 'api/login'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we move this file to the api directory too? Feels strange to have a api.rb and api directory. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually pretty standard in Ruby.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's common in Ruby. I'm fine with it if you are @chris-rock.

@@ -238,5 +241,13 @@ def self.server_version_from_config(config)
return nil unless config['version'].is_a?(Hash)
config['version']['version']
end

def self.determine_server_type(url, insecure)
if Compliance::HTTP.get(url + '/compliance/version', nil, insecure).code == '401'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this also return 200 if the user is already logged in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not passing any headers to the GET request so it should always act as if the user is not logged in.


case options['server_type']
when :automate
config = Login::AutomateServer.login(options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice separation!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Took some work that's for sure 😅

end

puts '', msg
Compliance::API.login(options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean-up! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! 😃

Copy link
Contributor

@chris-rock chris-rock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works like a charm!

@chris-rock chris-rock merged commit 91403d8 into master Oct 26, 2017
@chris-rock chris-rock deleted the ja/merge-login-commands branch October 26, 2017 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Enhancement Improves an existing feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Chef Automate support to inspec compliance login
4 participants