Skip to content

refactor: prepare for ContainerAuthenticator impl #139

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 1 commit into from
Aug 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 35 additions & 121 deletions src/main/java/com/ibm/cloud/sdk/core/security/IamAuthenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,19 @@
* an access token from the IAM token service via the "POST /identity/token" operation.
* When the access token expires, a new access token will be fetched.
*/
public class IamAuthenticator extends TokenRequestBasedAuthenticator<IamToken, IamToken> implements Authenticator {
public class IamAuthenticator extends IamRequestBasedAuthenticator implements Authenticator {
private static final String DEFAULT_IAM_URL = "https://iam.cloud.ibm.com";
private static final String OPERATION_PATH = "/identity/token";
private static final String GRANT_TYPE = "grant_type";
Copy link
Member Author

Choose a reason for hiding this comment

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

I removed these constants because they are used only in the requestToken() function and I actually think the code is more readable and understandable WITHOUT using these contants.

private static final String REQUEST_GRANT_TYPE = "urn:ibm:params:oauth:grant-type:apikey";
private static final String API_KEY = "apikey";
private static final String RESPONSE_TYPE = "response_type";
private static final String CLOUD_IAM = "cloud_iam";
private static final String SCOPE = "scope";

// Properties specific to an IAM authenticator.
private String url;
Copy link
Member Author

Choose a reason for hiding this comment

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

Most of the rest of the code deletions you see in this class are due to this code being moved to the new base class.

private String apikey;
private String scope;
private String clientId;
private String clientSecret;

// This is the value of the Authorization header we'll use when interacting with the token server.
private String cachedAuthorizationHeader = null;

/**
* This Builder class is used to construct IamAuthenticator instances.
*/
public static class Builder {
private String url;
private String apikey;
private String url;
private String scope;
private String clientId;
private String clientSecret;
Expand All @@ -70,12 +57,12 @@ public Builder() { }

// Builder ctor which copies config from an existing authenticator instance.
private Builder(IamAuthenticator obj) {
this.url = obj.url;
this.apikey = obj.apikey;
this.scope = obj.scope;
this.clientId = obj.clientId;
this.clientSecret = obj.clientSecret;

this.url = obj.getURL();
this.scope = obj.getScope();
this.clientId = obj.getClientId();
this.clientSecret = obj.getClientSecret();
this.disableSSLVerification = obj.getDisableSSLVerification();
this.headers = obj.getHeaders();
this.proxy = obj.getProxy();
Expand All @@ -91,6 +78,16 @@ public IamAuthenticator build() {
return new IamAuthenticator(this);
}

/**
* Sets the apikey property.
* @param apikey the apikey to use when retrieving an access token
* @return the Builder
*/
public Builder apikey(String apikey) {
this.apikey = apikey;
return this;
}

/**
* Sets the url property.
* @param url the base url to use with the IAM token service
Expand Down Expand Up @@ -121,16 +118,6 @@ public Builder clientSecret(String clientSecret) {
return this;
}

/**
* Sets the apikey property.
* @param apikey the apikey to use when retrieving an access token
* @return the Builder
*/
public Builder apikey(String apikey) {
this.apikey = apikey;
return this;
}

/**
* Sets the scope property.
* @param scope the scope to use when retrieving an access token
Expand Down Expand Up @@ -195,12 +182,11 @@ protected IamAuthenticator() {
* @param builder the Builder instance containing the configuration to be used
*/
protected IamAuthenticator(Builder builder) {
this.url = builder.url;
this.apikey = builder.apikey;
this.scope = builder.scope;
this.clientId = builder.clientId;
this.clientSecret = builder.clientSecret;

setURL(builder.url);
setScope(builder.scope);
setClientIdAndSecret(builder.clientId, builder.clientSecret);
setDisableSSLVerification(builder.disableSSLVerification);
setHeaders(builder.headers);
setProxy(builder.proxy);
Expand Down Expand Up @@ -319,8 +305,8 @@ public static IamAuthenticator fromConfiguration(Map<String, String> config) {
}

return new Builder()
.url(config.get(PROPNAME_URL))
.apikey(apikey)
.url(config.get(PROPNAME_URL))
.scope(config.get(PROPNAME_SCOPE))
.clientId(config.get(PROPNAME_CLIENT_ID))
.clientSecret(config.get(PROPNAME_CLIENT_SECRET))
Expand Down Expand Up @@ -350,7 +336,8 @@ public static IamAuthenticator fromConfiguration(Map<String, String> config) {
protected void init(String apikey, String url, String clientId, String clientSecret,
boolean disableSSLVerification, Map<String, String> headers, String scope) {
this.apikey = apikey;
this.url = url;

setURL(url);
setClientIdAndSecret(clientId, clientSecret);
setScope(scope);
this.validate();
Expand All @@ -361,13 +348,14 @@ protected void init(String apikey, String url, String clientId, String clientSec

@Override
public void validate() {
super.validate();
Copy link
Member Author

Choose a reason for hiding this comment

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

The new base class has a validate function that validates the common fields contained in that class.


if (StringUtils.isEmpty(this.url)) {
if (StringUtils.isEmpty(this.getURL())) {
// If no base URL was configured, then use the default IAM base URL.
this.url = DEFAULT_IAM_URL;
} else if (this.url.endsWith(OPERATION_PATH)) {
this.setURL(DEFAULT_IAM_URL);
} else {
// Canonicalize the URL by removing the operation path from it if present.
this.url = this.url.substring(0, this.url.length() - OPERATION_PATH.length());
this.setURL(StringUtils.removeEnd(this.getURL(), OPERATION_PATH));
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: StringUtils.removeEnd() will only remove OPERATION_PATH from the end of the string if the string in fact ends with OPERATION_PATH :)

}

if (StringUtils.isEmpty(this.apikey)) {
Expand All @@ -377,21 +365,6 @@ public void validate() {
if (CredentialUtils.hasBadStartOrEndChar(this.apikey)) {
throw new IllegalArgumentException(String.format(ERRORMSG_PROP_INVALID, "apikey"));
}

if (StringUtils.isEmpty(getUsername()) && StringUtils.isEmpty(getPassword())) {
// both empty is ok.
} else {
if (StringUtils.isEmpty(getUsername())) {
throw new IllegalArgumentException(String.format(ERRORMSG_PROP_MISSING, "clientId"));
}
if (StringUtils.isEmpty(getPassword())) {
throw new IllegalArgumentException(String.format(ERRORMSG_PROP_MISSING, "clientSecret"));
}
}

// Assuming everything validates clean, let's cache the basic auth header if
// the clientId/clientSecret properties are configured.
this.cachedAuthorizationHeader = constructBasicAuthHeader(this.clientId, this.clientSecret);
}

@Override
Expand All @@ -406,38 +379,6 @@ public String getApiKey() {
return this.apikey;
}

/**
* @return the URL configured on this Authenticator.
*/
public String getURL() {
return this.url;
}

/**
* Sets the URL on this Authenticator.
* @param url the URL representing the IAM token server endpoint
*/
public void setURL(String url) {
if (StringUtils.isEmpty(url)) {
url = DEFAULT_IAM_URL;
}
this.url = url;
}

/**
* @return the clientId configured on this Authenticator.
*/
public String getClientId() {
return this.clientId;
}

/**
* @return the clientSecret configured on this Authenticator.
*/
public String getClientSecret() {
return this.clientSecret;
}

/**
* @return the basic auth username (clientId) configured for this Authenticator.
*
Expand Down Expand Up @@ -470,32 +411,6 @@ public void setBasicAuthInfo(String clientId, String clientSecret) {
setClientIdAndSecret(clientId, clientSecret);
}

/**
* Sets the clientId and clientSecret on this Authenticator.
* @param clientId the clientId to use in interactions with the token server
* @param clientSecret the clientSecret to use in interactions with the token server
*/
public void setClientIdAndSecret(String clientId, String clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.validate();
}

/**
* @return the scope parameter
*/
public String getScope() {
return this.scope;
}

/**
* Sets the "scope" parameter to use when fetching the bearer token from the IAM token server.
* @param value a space seperated string that makes up the scope parameter.
*/
public void setScope(String value) {
this.scope = value;
}

/**
* Fetches an IAM access token for the apikey using the configured URL.
*
Expand All @@ -504,23 +419,22 @@ public void setScope(String value) {
@Override
public IamToken requestToken() {
// Form a POST request to retrieve the access token.
RequestBuilder builder = RequestBuilder.post(RequestBuilder.resolveRequestUrl(this.url, OPERATION_PATH));
RequestBuilder builder = RequestBuilder.post(RequestBuilder.resolveRequestUrl(this.getURL(), OPERATION_PATH));

// Now add the Content-Type and (optionally) the Authorization header to the token server request.
builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED);
if (StringUtils.isNotEmpty(this.cachedAuthorizationHeader)) {
builder.header(HttpHeaders.AUTHORIZATION, this.cachedAuthorizationHeader);
}
addAuthorizationHeader(builder);

// Build the form request body.
FormBody formBody;
final FormBody.Builder formBodyBuilder = new FormBody.Builder()
.add(GRANT_TYPE, REQUEST_GRANT_TYPE)
.add(API_KEY, apikey)
.add(RESPONSE_TYPE, CLOUD_IAM);
// Add the scope param if it's not empty
.add("grant_type", "urn:ibm:params:oauth:grant-type:apikey")
.add("apikey", getApiKey())
.add("response_type", "cloud_iam");

// Add the scope param if it's not empty.
if (!StringUtils.isEmpty(getScope())) {
formBodyBuilder.add(SCOPE, getScope());
formBodyBuilder.add("scope", getScope());
}
formBody = formBodyBuilder.build();
builder.body(formBody);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* (C) Copyright IBM Corp. 2015, 2021.
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the new base class. I'll leave the initial copyright year (2015) here since this code was copied from the other file.

*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package com.ibm.cloud.sdk.core.security;

import org.apache.commons.lang3.StringUtils;

import com.ibm.cloud.sdk.core.http.HttpHeaders;
import com.ibm.cloud.sdk.core.http.RequestBuilder;

/**
* This class contains code that is common to all authenticators that need to
* interact with the IAM tokens service to obtain an access token.
*/
public abstract class IamRequestBasedAuthenticator
extends TokenRequestBasedAuthenticator<IamToken, IamToken>
implements Authenticator {

private static final String DEFAULT_IAM_URL = "https://iam.cloud.ibm.com";

// Properties common to IAM-based authenticators.
private String url;
private String scope;
private String clientId;
private String clientSecret;

// This is the value of the Authorization header we'll use when interacting with the token server.
protected String cachedAuthorizationHeader = null;


@Override
public void validate() {
if (StringUtils.isEmpty(getClientId()) && StringUtils.isEmpty(getClientSecret())) {
// both empty is ok.
} else {
if (StringUtils.isEmpty(getClientId())) {
throw new IllegalArgumentException(String.format(ERRORMSG_PROP_MISSING, "clientId"));
}
if (StringUtils.isEmpty(getClientSecret())) {
throw new IllegalArgumentException(String.format(ERRORMSG_PROP_MISSING, "clientSecret"));
}
}

// Assuming everything validates clean, let's cache the basic auth header if
// the clientId/clientSecret properties are configured.
this.cachedAuthorizationHeader = constructBasicAuthHeader(this.clientId, this.clientSecret);
}

/**
* @return the URL configured on this Authenticator.
*/
public String getURL() {
return this.url;
}

/**
* Sets the URL on this Authenticator.
* @param url the URL representing the IAM token server endpoint
*/
public void setURL(String url) {
if (StringUtils.isEmpty(url)) {
url = DEFAULT_IAM_URL;
}
this.url = url;
}

/**
* @return the clientId configured on this Authenticator.
*/
public String getClientId() {
return this.clientId;
}

/**
* @return the clientSecret configured on this Authenticator.
*/
public String getClientSecret() {
return this.clientSecret;
}

/**
* Sets the clientId and clientSecret on this Authenticator.
* @param clientId the clientId to use in interactions with the token server
* @param clientSecret the clientSecret to use in interactions with the token server
*/
public void setClientIdAndSecret(String clientId, String clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.validate();
}

/**
* @return the scope parameter
*/
public String getScope() {
return this.scope;
}

/**
* Sets the "scope" parameter to use when fetching the bearer token from the IAM token server.
* @param value a space seperated string that makes up the scope parameter.
*/
public void setScope(String value) {
this.scope = value;
}

/**
* If a basic auth Authorization header is cached in "this", then add it to
* the specified request builder.
* @param builder the request builder
*/
protected void addAuthorizationHeader(RequestBuilder builder) {
if (StringUtils.isNotEmpty(this.cachedAuthorizationHeader)) {
builder.header(HttpHeaders.AUTHORIZATION, this.cachedAuthorizationHeader);
}
}
}