-
Notifications
You must be signed in to change notification settings - Fork 24
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
feat: add detailed error response to iam/cp4d authenticators #66
Conversation
After some discussion with @padamstx, there's some needed refactoring on my set of changes. I will re-request reviews once those changes are done. |
e906eb3
to
77c75b6
Compare
77c75b6
to
8147adc
Compare
This looks like it will be a breaking change, since it changes an external interface. If that's correct, we just need to make sure we include the right markers in the commit message so that semantic release does the right thing. |
Agree... I hadn't considered this angle yet, but we do in fact have code other than the Go core itself that explicitly calls the @jorge-ibm To do this, we'll need to do two things:
Then once this change has been merged in, we'll need to change the generator to use "v5" in the import path used for the core. Edit: Actually, I just thought of a possible way to avoid a new major version in this situation... |
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 think we can avoid a breaking change by keeping the return type as error
for the Authenticate() method, even though it will be returning an AuthenticationError
instance. This is because the AuthenticationError struct implements the "error" interface.
Please make sure that your tests include at least one scenario where we're explicitly calling Authenticate() and receiving the result as a "error".
v4/core/authenticator.go
Outdated
@@ -21,6 +21,16 @@ import ( | |||
// Authenticator describes the set of methods implemented by each authenticator. | |||
type Authenticator interface { | |||
AuthenticationType() string | |||
Authenticate(*http.Request) error | |||
Authenticate(*http.Request) *AuthenticationError |
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 think we can revert this back to error
as the return type in order to maintain compatibility in the API. This will mean that where we call Authenticate() in BaseService.Request(), you'll need to adjust that code to cast the "error" to an "AuthenticationError" when trying to retrieve the DetailedResponse field. I hadn't thought of this "trick" before, but should have :)
return | ||
authError := service.Options.Authenticator.Authenticate(req) | ||
if authError != nil { | ||
err = fmt.Errorf(ERRORMSG_AUTHENTICATE_ERROR, authError.Error()) |
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.
After initializing "err" on this line, you'd need to do a type assertion to cast "authErr" to be an AuthenticationError instance (we'll use castErr
in this example), then set detailedResponse = castErr.Response
.
v4/core/base_service.go
Outdated
authError := service.Options.Authenticator.Authenticate(req) | ||
if authError != nil { | ||
err = fmt.Errorf(ERRORMSG_AUTHENTICATE_ERROR, authError.Error()) | ||
return authError.Response, err |
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 just need to make sure that the detailedResponse
and err
named return values are assigned their correct values, then just do a naked return, like before.
return authError.Response, err | |
return |
v4/core/base_service_test.go
Outdated
assert.NotNil(t, detailedResponse.GetHeaders()) | ||
assert.NotNil(t, detailedResponse.GetRawResult()) | ||
statusCode := detailedResponse.GetStatusCode() | ||
assert.NotNil(t, statusCode) |
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.
statusCode is an int, so you can't really check it against nil
.
v4/core/base_service_test.go
Outdated
assert.NotNil(t, detailedResponse.GetRawResult()) | ||
statusCode := detailedResponse.GetStatusCode() | ||
headers := detailedResponse.GetHeaders() | ||
assert.NotNil(t, statusCode) |
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.
ditto
v4/core/base_service_test.go
Outdated
assert.NotNil(t, detailedResponse.GetHeaders()) | ||
assert.NotNil(t, detailedResponse.GetRawResult()) | ||
statusCode := detailedResponse.GetStatusCode() | ||
assert.NotNil(t, statusCode) |
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.
ditto
v4/core/base_service_test.go
Outdated
assert.NotNil(t, detailedResponse.GetRawResult()) | ||
statusCode := detailedResponse.GetStatusCode() | ||
headers := detailedResponse.GetHeaders() | ||
assert.NotNil(t, statusCode) |
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.
one more
d89fc2c
to
c7e1b32
Compare
v4/core/base_service.go
Outdated
if authError != nil { | ||
castErr, ok := authError.(*AuthenticationError) | ||
if !ok { | ||
err = fmt.Errorf(ERRORMSG_CAST_ERROR, err) |
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.
Casting technically shouldn't fail, but just in case.
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 think we probably don't need to worry too much about the !ok case and return a different error in that scenario, because that would cause us to lose the actual authentication error.
So perhaps we could do something like this, which essentially is "return the DetailedResponse if we can":
if authError != nil {
err = authError.Error()
castErr, ok := authError.(*AuthenticationError)
if ok {
detailedResponse = castErr.Response
}
return
}
@@ -40,6 +40,39 @@ func TestCp4dConfigErrors(t *testing.T) { | |||
assert.NotNil(t, err) | |||
} | |||
|
|||
func TestCp4dAuthenticateFail(t *testing.T) { |
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.
Adding this new test to test the error
returned from the Authenticate
method
c7e1b32
to
a9f0a25
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.
Jorge, first of all I apologize for not giving you better direction initially and I'm sure this PR seems like a wild goose chase at this point. Having said that I think there are a few more changes that would make this a little cleaner:
-
As much as possible, let's try to use
error
as the return type of various functions instead of*AuthenticationError
. I think we can useerror
for all function return types since AuthenticationError
implements theError()
function, but I'm open to being convinced otherwise :) -
Let's avoid creating any error structs ahead of time and just create them when/if we need to actually return an error.
-
By reverting all function return types back to using
error
, that means that we would need to return an actual instance of AuthenticationError ONLY if we also wanted to return an instance of DetailedResponse along with the error message. In situations where we will not be returning a DetailedResponse, we could simply return anerror
object instead.
I think these changes will make it cleaner and reduce the "blast radius" a bit.
v4/core/base_service.go
Outdated
if authError != nil { | ||
castErr, ok := authError.(*AuthenticationError) | ||
if !ok { | ||
err = fmt.Errorf(ERRORMSG_CAST_ERROR, err) |
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 think we probably don't need to worry too much about the !ok case and return a different error in that scenario, because that would cause us to lose the actual authentication error.
So perhaps we could do something like this, which essentially is "return the DetailedResponse if we can":
if authError != nil {
err = authError.Error()
castErr, ok := authError.(*AuthenticationError)
if ok {
detailedResponse = castErr.Response
}
return
}
assert.NotNil(t, detailedResponse.GetHeaders()) | ||
assert.NotNil(t, detailedResponse.GetRawResult()) | ||
statusCode := detailedResponse.GetStatusCode() | ||
assert.Equal(t, http.StatusForbidden, statusCode) |
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.
👍
v4/core/cp4d_authenticator.go
Outdated
@@ -150,7 +150,7 @@ func (authenticator *CloudPakForDataAuthenticator) Authenticate(request *http.Re | |||
// getToken: returns an access token to be used in an Authorization header. | |||
// Whenever a new token is needed (when a token doesn't yet exist, needs to be refreshed, | |||
// or the existing token has expired), a new access token is fetched from the token server. | |||
func (authenticator *CloudPakForDataAuthenticator) getToken() (string, error) { | |||
func (authenticator *CloudPakForDataAuthenticator) getToken() (string, *AuthenticationError) { |
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 think we can revert this back to just error
now that AuthenticationError implements the error
interface.
v4/core/cp4d_authenticator.go
Outdated
@@ -159,7 +159,7 @@ func (authenticator *CloudPakForDataAuthenticator) getToken() (string, error) { | |||
} | |||
} else if authenticator.tokenData.needsRefresh() { | |||
// If refresh needed, kick off a go routine in the background to get a new token | |||
ch := make(chan error) | |||
ch := make(chan *AuthenticationError) |
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 think we can also revert this back to error
as well.
v4/core/cp4d_authenticator.go
Outdated
@@ -172,9 +172,13 @@ func (authenticator *CloudPakForDataAuthenticator) getToken() (string, error) { | |||
} | |||
} | |||
|
|||
authError := &AuthenticationError{} |
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 think we should initialize authError only if we're going to return an actual error.
authError := &AuthenticationError{} |
b3e92c9
to
40f0b71
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.
Looks great!
I'm glad we were able to make this change without changing the return types in function signatures.
# [4.2.0](v4.1.0...v4.2.0) (2020-08-14) ### Features * add detailed error response to iam/cp4d authenticators ([#66](#66)) ([3485263](3485263))
🎉 This PR is included in version 4.2.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This PR creates a
DetailedResponse
instance for both the IAM/CP4D authenticators in the case where the api call to either servers fails (returns a status code we consider a failure). ThisDetailedResponse
instance contains the response status code, headers, and the raw byte result.I've created a
AuthenticationError
struct that contains an instance ofDetailedResponse
along with theerr
field which contains the actual error object. Whenever we expect a detailed response to be returned from theAuthenticate
method due to an error, we attempt to cast the returned error intoAuthenticationError
type. If successful, the detailed response is included in the returned value for the Base Service'sRequest
method.ref: https://github.ibm.com/arf/planning-sdk-squad/issues/2030