-
Notifications
You must be signed in to change notification settings - Fork 97
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
Fixes #25848 - Adding SSO func. through CLI using OpenID Connect. #405
Conversation
@authenticator = InteractiveBasicAuth.new(username, password) | ||
end | ||
@authenticator | ||
@authenticator = InteractiveTokenAuth.new(settings.get(:_params, :token) || ENV['KEYCLOAK_TOKEN']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mbacovsky I am trying to create an object of the class InteractiveTokenAuth
but i am failing in doing so. Can you please take a look?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole idea is to set the @token in the @authenticator
so that it can be passed to foreman api.
@mbacovsky you need to use the command For username and password, you will have to create a username and password on keycloak in a defined realm. Let me know if you need any help with the setup :) |
54f90a7
to
7a93083
Compare
@mbacovsky after Apipie/apipie-bindings#77, I guess except the clean up required, the pr is ready for testing. It works for me, let me know if you are facing any issues while testing it. Ah! right now it takes the expiration time of the session from foreman settings, I need to fix that! |
641bda7
to
347bf39
Compare
require 'hammer_cli_foreman/api/void_auth' | ||
|
||
module HammerCLIForeman | ||
module Api | ||
class Connection < HammerCLI::Apipie::ApiConnection | ||
attr_reader :authenticator | ||
|
||
def initialize(settings, logger = nil, locale = nil) | ||
default_params = build_default_params(settings, logger, locale) | ||
def initialize(settings, logger = nil, locale = nil, auth_type = AuthTypes[:basic_auth]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use constants instead of the hash for auth_type accross the whole PR? Something like AUTH_TYPE_BASIC, AUTH_TYPE_OAUTH_BASIC, AUTH_TYPE_OAUTH_2FA would be more readable.
lib/hammer_cli_foreman/commands.rb
Outdated
@@ -66,7 +66,7 @@ def self.record_to_common_format(data) | |||
class Command < HammerCLI::Apipie::Command | |||
|
|||
def self.connection_name(resource_class) | |||
CONNECTION_NAME | |||
HammerCLIForeman.get_connection_name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have different connection names for different auth methods? This name should refer to which API we are connecting and should be immutable for a resource.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We observed that HammerCLIForeman.foreman_api_connection is being called multiple times when we run any of the login commands.
def self.foreman_api_connection
HammerCLI.context[:api_connection].create(CONNECTION_NAME) do
HammerCLIForeman::Api::Connection.new(HammerCLI::Settings, Logging.logger['API'], HammerCLI::I18n.locale)
end
end
If we look at this method, it yields a block only if a previous connection with name {CONNECTION_NAME} (foreman) has not been called already.
From HammerCli::Connection.rb:
def create(name, &create_connector_block)
unless connections[name]
connector = yield
@logger.debug("Registered: #{name}") if @logger
connections[name] = connector
end
connections[name]
end
This worked fine in our initial flow since there was only a single type of authentication flow available (ie basic auth).
When we introduced more authentication ways, and called HammerCLIForeman.foreman_api_connection
from our auth.rb file, for other authentication ways, the block was never yielded, because the first time HammerCLIForeman.foreman_api_connection
was called, the connection (with basic_auth) was already stored with FOREMAN
key.
Hence we did these changes, so that the block is yielded, when we use any other authentiction way except basic_auth.
I will be glad to explain this in more detail, along with the flow as required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rahulbajaj0509 thanks for all the effort on this PR(s). Overall the code looks good and works fine. I'm only concerned about how the new connections are created and a few minor things. I also have a few usability improvements. We discussed that already so I'll put it here for the record. More details inline. My concerns are:
- we shouldn't change the connection name but rather replace the existing one
- foreman_api_connection shouldn't have any parameters
- default auth_type should be configurable in settings
- session could hold the auth_type for proper resume when it expires
- can we use the refresh token for refresh with the information?
- will require change in how the expiration is handled (remove session_id from session vs. do re-login before hammer reload)
- consider making the sso CA configurable
execute_with_params
to a module- better cli command names
If there is anything unclear or you find any blockers, let me know.
def self.foreman_api_connection | ||
HammerCLI.context[:api_connection].create(CONNECTION_NAME) do | ||
HammerCLIForeman::Api::Connection.new(HammerCLI::Settings, Logging.logger['API'], HammerCLI::I18n.locale) | ||
def self.foreman_api_connection(auth_type = AuthTypes[:basic_auth]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method should get a connection or create one. The caller should not know anything about the auth_type.
In Login command we should clear the connection and create new one of proper type when we know the type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could add self.foreman_api_reconnect
that will drop the connection and create new one of desired auth_type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The connection drop could be achieved by HammerCLI.context[:api_connection].drop(CONNECTION_NAME)
HammerCLI.context[:api_connection].create(CONNECTION_NAME) do | ||
HammerCLIForeman::Api::Connection.new(HammerCLI::Settings, Logging.logger['API'], HammerCLI::I18n.locale) | ||
def self.foreman_api_connection(auth_type = AuthTypes[:basic_auth]) | ||
connection = get_connection_name(auth_type) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason to rename the connection, we are still connecting to the Foreman API.
lib/hammer_cli_foreman/auth.rb
Outdated
def execute | ||
if !(HammerCLIForeman.foreman_api_connection.authenticator.is_a?(HammerCLIForeman::Api::SessionAuthenticatorWrapper)) | ||
print_message(_("Can't perform login. Make sure sessions are enabled in hammer configuration file.")) | ||
def self.execute_with_params(auth_type, *args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method should be in some module as we should not hijack the Login subcommand for that
lib/hammer_cli_foreman/auth.rb
Outdated
if !(HammerCLIForeman.foreman_api_connection.authenticator.is_a?(HammerCLIForeman::Api::SessionAuthenticatorWrapper)) | ||
print_message(_("Can't perform login. Make sure sessions are enabled in hammer configuration file.")) | ||
def self.execute_with_params(auth_type, *args) | ||
connection = HammerCLIForeman.foreman_api_connection(auth_type) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should call foreman_api_reconnect(auth_type)
a223c6d
to
fd78342
Compare
Remaining tasks for this pr are:
As pointed out by @mbacovsky, few more tasks:
These tasks are the total tasks remaining for this pr, I will tick each of them once the task is complete. |
def initialize(settings, logger = nil, locale = nil) | ||
default_params = build_default_params(settings, logger, locale) | ||
def initialize(settings, logger = nil, locale = nil, auth_type = | ||
AUTH_TYPES[HammerCLI::Settings.get(:foreman, :default_auth_type).to_sym]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the settings does not contain the :default_auth_type I get
NoMethodError (undefined method
to_sym' for nil:NilClass)`. We will need some default value here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it is a known issue. I figured that out :) Need to fix this!
|
After |
After some more login/logout excersises I'm getting correct output from |
if Dir[session_repo+ "/*"].empty? | ||
nil | ||
else | ||
Dir.entries(session_repo)[-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not safe for a couple of reasons. I have multiple session files for multiple foremans I manage in my session dir. We should pick the correct one by url, otherwise we can end up reading wrong file.
Also while this is not predictably sorted list I ended up with '..' on the last position resulting in ugly errors when reading the session.
I wonder if it is time to represent the session file and operations with some class and have the usage unified across the codebase.
Would something with similar interface make sense?:
session = HammerCLIForeman::Sessions.load(url) # load or create new session
expired = session.expired?
session.expire!
session.username
session.username = username
session.auth_type
session.auth_type = auth_type
session.save
session.id
session.id = id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah! completely makes sense. I was actually assuming that a user will have only one session file and one foreman instance to manage. Thanks for bringing this up :)
Your suggestion about making a new class that takes care of the session file and operation completely makes sense.
@rahulbajaj0509 Facing following issues:
|
f243aa7
to
98759f5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to login/out with both basic and oauth. Just a small nitpick (see inline).
Just one more thing. Is it okay that if user uses two factor auth and if wants to check status via |
|
||
def error(ex) | ||
if ex.is_a?(RestClient::InternalServerError) | ||
self.clear |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When logging in with invalid password I get
[ERROR 2019-10-25T15:37:22 Exception] Error: undefined method clear' for #<HammerCLIForeman::Api::Oauth::PasswordGrant:0x0000000002fb0098> Error: undefined method
clear' for #HammerCLIForeman::Api::Oauth::PasswordGrant:0x0000000002fb0098
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The clear should probably remove the values from the object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And that's why we need two reviewers... When I looked at the code I didn't notice that there is no method clear
for AuthenticationCodeGrant
and PasswordGrant
objects.
@rahulbajaj0509, is it a leftover or you forget to define those methods for PasswordGrant
and AuthenticationCodeGrant
objects? This method exists only in InteractiveBasicAuth
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see my above comment (if this is a problem we could fix it later).
Otherwise I have no other required changes for code, so you have finally my approve. Outstanding work! :)
Athough I wanted @mbacovsky to be agreed on everything and push the big green button after he is ready :)
@ofedoren it will ask for code only if the session has expired. |
6072401
to
6e88224
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm adding my 👍 finally. I did some more testing and didn't found any breaking changes. I also tested various combinations of parms on CLI and in settings and everything works as expected.
The error messages now look good too and provide useful information to the user.
@rahulbajaj0509 thanks for all the amazing effort you've put into this feature! |
🍻 🎆 🍰 💯 comments |
Thanks a lot for your support @mbacovsky. I was stuck so many times when thinking about foreman_ap_reconnect, sessions, getting the correct flows in order and it was all possible because of your constant push. I made so many mistakes and thanks for catching them all @ofedoren! :P thanks for testing the PR again and again. I know it can be really tedious to test a PR of this size repeatedly. Thanks a lot @mbacovsky @ofedoren for your valuable inputs, without you folks I wouldn't have made it 😄 |
@rahulbajaj0509 @mbacovsky
Traceback - https://paste.fedoraproject.org/paste/NDIeSMgm-uYi9QkOT0rCsw I am not sure this is related to PR but, how can I resolve it?
|
option ["-a", "--oidc-authorization-endpoint"], "OPENIDC-AUTHORIZATION-ENDPOINT", _("Openidc provider URL which issues authentication code") | ||
option ["-c", "--oidc-client-id"], "OPENIDC-CLIENT-ID", _("Client id used in the Openidc provider") | ||
option ["-f", "--two-factor"], :flag, _("Authenticate with two factor") | ||
option ["-r", "--oidc-redirect-uri"], "OPENIDC-REDIRECT-URI", _("Redirect URI for the authencation code grant flow") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo s/authencation/authentication
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch!
@ntkathole thanks for having a look 👍
This issue is not reproducible in my Foreman instance. Let me help you with this once I am back :)
Good catch! I will fix this :) |
My attempt is to use keycloak for authenticating the user
and fetching the token, once we have the token we can send this
token to the foreman api, where we can use the
JWT
gem tovalidate the token and then create a session for the user.
In this pr, I have attempted to fetch the token from keycloak using
the rest-client gem. This token has to be passed to foreman where
it will be validated and accordingly authentication would be a success/failure.