Mercurial > hg > thermostat-ng > agent
changeset 2751:f31a81430347
Split HttpRequestService and KeycloakAccessTokenService
Reviewed-by: jkang
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024882.html
line wrap: on
line diff
--- a/agent/core/pom.xml Thu Sep 07 18:41:40 2017 +0200 +++ b/agent/core/pom.xml Wed Sep 06 19:47:49 2017 +0200 @@ -130,7 +130,7 @@ <Export-Package> com.redhat.thermostat.agent, com.redhat.thermostat.agent.http, - com.redhat.thermostat.agent.http.internal.keycloak, + com.redhat.thermostat.agent.keycloak, com.redhat.thermostat.agent.config, com.redhat.thermostat.agent.dao, com.redhat.thermostat.agent.utils, @@ -139,6 +139,8 @@ </Export-Package> <Private-Package> com.redhat.thermostat.agent.internal, + com.redhat.thermostat.agent.internal.http, + com.redhat.thermostat.agent.internal.locale, com.redhat.thermostat.backend.internal, com.redhat.thermostat.utils.management.internal </Private-Package>
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpClientFacade.java Thu Sep 07 18:41:40 2017 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -/* - * Copyright 2012-2017 Red Hat, Inc. - * - * This file is part of Thermostat. - * - * Thermostat is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published - * by the Free Software Foundation; either version 2, or (at your - * option) any later version. - * - * Thermostat is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Thermostat; see the file COPYING. If not see - * <http://www.gnu.org/licenses/>. - * - * Linking this code with other modules is making a combined work - * based on this code. Thus, the terms and conditions of the GNU - * General Public License cover the whole combination. - * - * As a special exception, the copyright holders of this code give - * you permission to link this code with independent modules to - * produce an executable, regardless of the license terms of these - * independent modules, and to copy and distribute the resulting - * executable under terms of your choice, provided that you also - * meet, for each linked independent module, the terms and conditions - * of the license of that module. An independent module is a module - * which is not derived from or based on this code. If you modify - * this code, you may extend this exception to your version of the - * library, but you are not obligated to do so. If you do not wish - * to do so, delete this exception statement from your version. - */ - -package com.redhat.thermostat.agent.http; - -import java.net.URI; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.net.ssl.SSLContext; - -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import com.redhat.thermostat.common.ssl.SSLContextFactory; -import com.redhat.thermostat.common.ssl.SslInitException; -import com.redhat.thermostat.common.utils.LoggingUtils; -import com.redhat.thermostat.shared.config.SSLConfiguration; - -class HttpClientFacade { - - private static final Logger logger = LoggingUtils.getLogger(HttpClientFacade.class); - private static final long PER_REQUEST_TIMEOUT_SEC = 5; - private final HttpClient httpsClient; - - HttpClientFacade(SSLConfiguration sslConfig) { - httpsClient = createHttpsClient(sslConfig); - } - - private static HttpClient createHttpsClient(SSLConfiguration config) { - try { - SSLContext context = SSLContextFactory.getClientContext(config); - SslContextFactory sslFactory = new SslContextFactory(); - sslFactory.setSslContext(context); - // Don't send SSLv2 Client Hello. Some servers will refuse to - // accept it. So does the web-gateway with our self-signed cert. - sslFactory.setIncludeProtocols("TLSv1", "TLSv1.2"); - if (config.disableHostnameVerification()) { - logger.fine("HTTPS endpoint verification disabled."); - } else { - sslFactory.setEndpointIdentificationAlgorithm("HTTPS"); - } - HttpClient client = new HttpClient(sslFactory); - return client; - } catch (SslInitException e) { - logger.log(Level.INFO, "Failed to initialize SSL context.", e); - logger.severe("Failed to initialize SSL context. Reason: " + e.getMessage()); - throw new RuntimeException(e); - } - } - - void start() throws Exception { - httpsClient.start(); - } - - Request newRequest(URI uri) { - return httpsClient.newRequest(uri).timeout(PER_REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS); - } - - Request newRequest(String url) { - return httpsClient.newRequest(url).timeout(PER_REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS); - } -}
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpRequestService.java Thu Sep 07 18:41:40 2017 +0200 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpRequestService.java Wed Sep 06 19:47:49 2017 +0200 @@ -37,82 +37,14 @@ package com.redhat.thermostat.agent.http; -import java.io.IOException; import java.net.URI; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.felix.scr.annotations.Activate; -import org.apache.felix.scr.annotations.Component; -import org.apache.felix.scr.annotations.Reference; -import org.apache.felix.scr.annotations.Service; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.http.HttpStatus; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.redhat.thermostat.agent.config.AgentConfigsUtils; -import com.redhat.thermostat.agent.config.AgentStartupConfiguration; -import com.redhat.thermostat.agent.http.internal.keycloak.KeycloakAccessToken; -import com.redhat.thermostat.common.utils.LoggingUtils; -import com.redhat.thermostat.shared.config.CommonPaths; -import com.redhat.thermostat.shared.config.SSLConfiguration; -import com.redhat.thermostat.storage.config.FileStorageCredentials; -import com.redhat.thermostat.storage.core.StorageCredentials; - -@Component -@Service(value = HttpRequestService.class) -public class HttpRequestService { - - private static final Logger logger = LoggingUtils.getLogger(HttpRequestService.class); - private static final String UNKNOWN_CREDS = "UNKNOWN:UNKNOWN"; - private static final String KEYCLOAK_TOKEN_SERVICE = "/auth/realms/__REALM__/protocol/openid-connect/token"; - private static final String KEYCLOAK_CONTENT_TYPE = "application/x-www-form-urlencoded"; - - private final CredentialsCreator credsCreator; // Used for BASIC auth - private final HttpClientCreator httpClientCreator; - private final ConfigCreator configCreator; - private Gson gson = new GsonBuilder().create(); - private HttpClientFacade client; - private AgentStartupConfiguration agentStartupConfiguration; - private StorageCredentials creds; // Used for BASIC auth - private KeycloakAccessToken keycloakAccessToken; - - @Reference - private SSLConfiguration sslConfig; - @Reference - private CommonPaths commonPaths; +import com.redhat.thermostat.annotations.Service; - public HttpRequestService() { - this(new HttpClientCreator(), new ConfigCreator(), new CredentialsCreator()); - } - - HttpRequestService(HttpClientCreator clientCreator, ConfigCreator configCreator, CredentialsCreator credsCreator) { - this.httpClientCreator = clientCreator; - this.configCreator = configCreator; - this.credsCreator = credsCreator; - } - - @Activate - public void activate() { - try { - agentStartupConfiguration = configCreator.create(commonPaths); - client = httpClientCreator.create(sslConfig); - creds = credsCreator.create(commonPaths); - client.start(); - logger.log(Level.FINE, "HttpRequestService activated"); - } catch (Exception e) { - logger.log(Level.SEVERE, "HttpRequestService failed to start correctly. Behaviour undefined.", e); - } - } +@Service +public interface HttpRequestService { /** * Send a HTTP request @@ -121,186 +53,8 @@ * @param requestMethod The HTTP request type: GET, PUT, POST or DELETE * @return The returned body for GET requests. {@code null} otherwise. */ - public String sendHttpRequest(String jsonPayload, URI uri, Method requestMethod) throws RequestFailedException { - // Normalize URI to ensure any duplicate slashes are removed - uri = uri.normalize(); - Request request = client.newRequest(uri); - if (jsonPayload != null) { - request.content(new StringContentProvider(jsonPayload), "application/json"); - } - request.method(requestMethod.getHttpMethod()); - - try { - if (agentStartupConfiguration.isKeycloakEnabled()) { - request.header(HttpHeader.AUTHORIZATION.asString(), "Bearer " + getAccessToken()); - } else if (agentStartupConfiguration.isBasicAuthEnabled()) { - request.header(HttpHeader.AUTHORIZATION.asString(), - getBasicAuthHeaderValue()); - } else { - logger.warning("Neither KEYCLOAK_ENABLED=true nor BASIC_AUTH_ENABLED=true. Requests will probably fail."); - } - ContentResponse response = request.send(); - int status = response.getStatus(); - if (status != HttpStatus.OK_200) { - throw new RequestFailedException(status, "Request to gateway failed. Reason: " + response.getReason()); - } - if (requestMethod == Method.GET) { - return response.getContentAsString(); - } else { - return null; - } - } catch (InterruptedException | TimeoutException | IOException | ExecutionException e) { - throw new RequestFailedException(e); - } - } - - private String getAccessToken() throws IOException { - if (keycloakAccessToken == null) { - keycloakAccessToken = acquireKeycloakToken(); - } else if (isKeycloakTokenExpired()) { - logger.log(Level.FINE, "Keycloak Token expired attempting to reacquire via refresh_token"); - keycloakAccessToken = refreshKeycloakToken(); - - if (keycloakAccessToken == null) { - logger.log(Level.WARNING, "Unable to refresh Keycloak token, attempting to acquire new token"); - keycloakAccessToken = acquireKeycloakToken(); - } - - if (keycloakAccessToken == null) { - logger.log(Level.SEVERE, "Unable to reacquire KeycloakToken."); - throw new IOException("Keycloak token expired and attempt to refresh and reacquire Keycloak token failed."); - } - } - - return keycloakAccessToken.getAccessToken(); - } - - private boolean isKeycloakTokenExpired() { - return System.nanoTime() > TimeUnit.NANOSECONDS.convert(keycloakAccessToken.getExpiresIn(), TimeUnit.SECONDS) + keycloakAccessToken.getAcquireTime(); - } - - private KeycloakAccessToken acquireKeycloakToken() { - return requestKeycloakToken(getKeycloakAccessPayload()); - } - - private KeycloakAccessToken refreshKeycloakToken() { - return requestKeycloakToken(getKeycloakRefreshPayload()); - } - - private KeycloakAccessToken requestKeycloakToken(String payload) { - String url = agentStartupConfiguration.getKeycloakUrl() + KEYCLOAK_TOKEN_SERVICE.replace("__REALM__", agentStartupConfiguration.getKeycloakRealm()); - Request request = client.newRequest(url); - request.content(new StringContentProvider(payload), KEYCLOAK_CONTENT_TYPE); - request.method(HttpMethod.POST); - - try { - ContentResponse response = request.send(); - if (response.getStatus() == HttpStatus.OK_200) { - - String content = response.getContentAsString(); - - keycloakAccessToken = gson.fromJson(content, KeycloakAccessToken.class); - keycloakAccessToken.setAcquireTime(System.nanoTime()); - - logger.log(Level.FINE, "Keycloak Token acquired"); - return keycloakAccessToken; - } else { - logger.log(Level.WARNING, "Failed to acquire Keycloak token: " + response.getStatus() + " " + response.getReason()); - } - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to acquire Keycloak token", e); - } - return null; - } - - private String getKeycloakAccessPayload() { - String username = creds.getUsername(); - String password = new String(creds.getPassword()); - return "grant_type=password&client_id=" + agentStartupConfiguration.getKeycloakClient() + - "&username=" + username + - "&password=" + password; - } - - private String getKeycloakRefreshPayload() { - return "grant_type=refresh_token&client_id=" + agentStartupConfiguration.getKeycloakClient() + - "&refresh_token=" + keycloakAccessToken.getRefreshToken(); - } + public String sendHttpRequest(String jsonPayload, URI uri, Method requestMethod) throws RequestFailedException; - private String getBasicAuthHeaderValue() { - String username = creds.getUsername(); - char[] pwdChar = creds.getPassword(); - String userpassword; - if (username == null || username.isEmpty() || pwdChar == null) { - logger.warning("No credentials specified in " + commonPaths.getUserAgentAuthConfigFile() + ". The connection will fail."); - userpassword = UNKNOWN_CREDS; - } else { - String pwd = new String(pwdChar); - userpassword = username + ":" + pwd; - } - - @SuppressWarnings("restriction") - String encodedAuthorization = new sun.misc.BASE64Encoder() - .encode(userpassword.getBytes()); - return "Basic " + encodedAuthorization; - } - - // Package-private for testing - void setConfiguration(AgentStartupConfiguration configuration) { - this.agentStartupConfiguration = configuration; - } - - static class HttpClientCreator { - - HttpClientFacade create(SSLConfiguration config) { - return new HttpClientFacade(config); - } - - } - - static class ConfigCreator { - AgentStartupConfiguration create(CommonPaths commonPaths) { - AgentConfigsUtils.setConfigFiles(commonPaths.getSystemAgentConfigurationFile(), - commonPaths.getUserAgentConfigurationFile()); - return AgentConfigsUtils.createAgentConfigs(); - } - } - - static class CredentialsCreator { - StorageCredentials create(CommonPaths paths) { - return new FileStorageCredentials(paths.getUserAgentAuthConfigFile()); - } - } - - @SuppressWarnings("serial") - public static class RequestFailedException extends Exception { - - public static final int UNKNOWN_RESPONSE_CODE = -1; - private final int responseCode; - private final String reasonStr; - - private RequestFailedException(Throwable e) { - this(UNKNOWN_RESPONSE_CODE, e.getMessage(), e); - } - - private RequestFailedException(int responseCode, String reason) { - this(responseCode, reason, null); - } - - private RequestFailedException(int responseCode, String reason, Throwable cause) { - super(reason, cause); - this.reasonStr = reason; - this.responseCode = responseCode; - } - - public int getResponseCode() { - return responseCode; - } - - public String getReason() { - return reasonStr; - } - } - /** * HTTP methods for microservice requests. * @author mzezulka
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/http/RequestFailedException.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.http; + +@SuppressWarnings("serial") +public class RequestFailedException extends Exception { + + public static final int UNKNOWN_RESPONSE_CODE = -1; + private final int responseCode; + private final String reasonStr; + + public RequestFailedException(Throwable e) { + this(UNKNOWN_RESPONSE_CODE, e.getMessage(), e); + } + + public RequestFailedException(String message) { + this(UNKNOWN_RESPONSE_CODE, message); + } + + public RequestFailedException(int responseCode, String reason) { + this(responseCode, reason, null); + } + + public RequestFailedException(int responseCode, String reason, Throwable cause) { + super(reason, cause); + this.reasonStr = reason; + this.responseCode = responseCode; + } + + public int getResponseCode() { + return responseCode; + } + + public String getReason() { + return reasonStr; + } +} \ No newline at end of file
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/http/internal/keycloak/KeycloakAccessToken.java Thu Sep 07 18:41:40 2017 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2017 Red Hat, Inc. - * - * This file is part of Thermostat. - * - * Thermostat is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published - * by the Free Software Foundation; either version 2, or (at your - * option) any later version. - * - * Thermostat is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Thermostat; see the file COPYING. If not see - * <http://www.gnu.org/licenses/>. - * - * Linking this code with other modules is making a combined work - * based on this code. Thus, the terms and conditions of the GNU - * General Public License cover the whole combination. - * - * As a special exception, the copyright holders of this code give - * you permission to link this code with independent modules to - * produce an executable, regardless of the license terms of these - * independent modules, and to copy and distribute the resulting - * executable under terms of your choice, provided that you also - * meet, for each linked independent module, the terms and conditions - * of the license of that module. An independent module is a module - * which is not derived from or based on this code. If you modify - * this code, you may extend this exception to your version of the - * library, but you are not obligated to do so. If you do not wish - * to do so, delete this exception statement from your version. - */ - -package com.redhat.thermostat.agent.http.internal.keycloak; - -import com.google.gson.annotations.SerializedName; - -public class KeycloakAccessToken { - @SerializedName("access_token") - private String accessToken; - - // In seconds - @SerializedName("expires_in") - private long expiresIn; - - // In seconds - @SerializedName("refresh_expires_in") - private long refreshExpiresIn; - - @SerializedName("refresh_token") - private String refreshToken; - - // In nanoseconds - private transient long acquireTime; - - - public String getAccessToken() { - return accessToken; - } - - public Long getExpiresIn() { - return expiresIn; - } - - public Long getRefreshExpiresIn() { - return refreshExpiresIn; - } - - public String getRefreshToken() { - return refreshToken; - } - - public long getAcquireTime() { - return acquireTime; - } - - public void setAcquireTime(long acquireTime) { - this.acquireTime = acquireTime; - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/internal/http/BasicHttpService.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.internal.http; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.redhat.thermostat.agent.config.AgentConfigsUtils; +import com.redhat.thermostat.agent.config.AgentStartupConfiguration; +import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.shared.config.CommonPaths; +import com.redhat.thermostat.shared.config.SSLConfiguration; +import com.redhat.thermostat.storage.config.FileStorageCredentials; +import com.redhat.thermostat.storage.core.StorageCredentials; + +class BasicHttpService { + + private static final Logger logger = LoggingUtils.getLogger(BasicHttpService.class); + protected final CredentialsCreator credsCreator; + protected final HttpClientCreator httpClientCreator; + protected final ConfigCreator configCreator; + protected HttpClientFacade client; + protected AgentStartupConfiguration agentStartupConfiguration; + protected StorageCredentials creds; + + BasicHttpService() { + this(new HttpClientCreator(), new ConfigCreator(), new CredentialsCreator()); + } + + BasicHttpService(HttpClientCreator clientCreator, ConfigCreator configCreator, CredentialsCreator credsCreator) { + this.httpClientCreator = clientCreator; + this.configCreator = configCreator; + this.credsCreator = credsCreator; + } + + protected void doActivate(CommonPaths commonPaths, SSLConfiguration sslConfig) { + try { + agentStartupConfiguration = configCreator.create(commonPaths); + client = httpClientCreator.create(sslConfig); + creds = credsCreator.create(commonPaths); + client.start(); + logger.log(Level.FINE, "HttpRequestService activated"); + } catch (Exception e) { + logger.log(Level.SEVERE, "HttpRequestService failed to start correctly. Behaviour undefined.", e); + } + } + + static class ConfigCreator { + AgentStartupConfiguration create(CommonPaths commonPaths) { + AgentConfigsUtils.setConfigFiles(commonPaths.getSystemAgentConfigurationFile(), + commonPaths.getUserAgentConfigurationFile()); + return AgentConfigsUtils.createAgentConfigs(); + } + } + + static class CredentialsCreator { + StorageCredentials create(CommonPaths paths) { + return new FileStorageCredentials(paths.getUserAgentAuthConfigFile()); + } + } + + static class HttpClientCreator { + + HttpClientFacade create(SSLConfiguration config) { + return new HttpClientFacade(config); + } + + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/internal/http/HttpClientFacade.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.internal.http; + +import java.net.URI; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLContext; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import com.redhat.thermostat.common.ssl.SSLContextFactory; +import com.redhat.thermostat.common.ssl.SslInitException; +import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.shared.config.SSLConfiguration; + +class HttpClientFacade { + + private static final Logger logger = LoggingUtils.getLogger(HttpClientFacade.class); + private static final long PER_REQUEST_TIMEOUT_SEC = 5; + private final HttpClient httpsClient; + + HttpClientFacade(SSLConfiguration sslConfig) { + httpsClient = createHttpsClient(sslConfig); + } + + private static HttpClient createHttpsClient(SSLConfiguration config) { + try { + SSLContext context = SSLContextFactory.getClientContext(config); + SslContextFactory sslFactory = new SslContextFactory(); + sslFactory.setSslContext(context); + // Don't send SSLv2 Client Hello. Some servers will refuse to + // accept it. So does the web-gateway with our self-signed cert. + sslFactory.setIncludeProtocols("TLSv1", "TLSv1.2"); + if (config.disableHostnameVerification()) { + logger.fine("HTTPS endpoint verification disabled."); + } else { + sslFactory.setEndpointIdentificationAlgorithm("HTTPS"); + } + HttpClient client = new HttpClient(sslFactory); + return client; + } catch (SslInitException e) { + logger.log(Level.INFO, "Failed to initialize SSL context.", e); + logger.severe("Failed to initialize SSL context. Reason: " + e.getMessage()); + throw new RuntimeException(e); + } + } + + void start() throws Exception { + httpsClient.start(); + } + + Request newRequest(URI uri) { + return httpsClient.newRequest(uri).timeout(PER_REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS); + } + + Request newRequest(String url) { + return httpsClient.newRequest(url).timeout(PER_REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/internal/http/HttpRequestServiceImpl.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.internal.http; + + +import java.net.URI; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; + +import com.redhat.thermostat.agent.http.HttpRequestService; +import com.redhat.thermostat.agent.http.RequestFailedException; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessToken; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessTokenService; +import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.shared.config.CommonPaths; +import com.redhat.thermostat.shared.config.SSLConfiguration; + +@Component +@Service(value = HttpRequestService.class) +public class HttpRequestServiceImpl extends BasicHttpService implements HttpRequestService { + + private static final Logger logger = LoggingUtils.getLogger(HttpRequestServiceImpl.class); + + private static final String UNKNOWN_CREDS = "UNKNOWN:UNKNOWN"; + + @Reference + private SSLConfiguration sslConfig; + @Reference + private CommonPaths commonPaths; + @Reference + private KeycloakAccessTokenService tokenService; + + public HttpRequestServiceImpl() { + this(new HttpClientCreator(), new ConfigCreator(), new CredentialsCreator()); + } + + // For testing purposes + HttpRequestServiceImpl(HttpClientCreator clientCreator, ConfigCreator configCreator, CredentialsCreator credsCreator) { + super(clientCreator, configCreator, credsCreator); + } + + @Activate + public void activate() { + super.doActivate(commonPaths, sslConfig); + } + + /** + * Send a HTTP request + * @param jsonPayload The payload to send, or null if no payload + * @param uri The complete URI to send to + * @param requestMethod The HTTP request type: GET, PUT, POST or DELETE + * @return The returned body for GET requests. {@code null} otherwise. + */ + public String sendHttpRequest(String jsonPayload, URI uri, Method requestMethod) throws RequestFailedException { + // Normalize URI to ensure any duplicate slashes are removed + uri = uri.normalize(); + Request request = client.newRequest(uri); + if (jsonPayload != null) { + request.content(new StringContentProvider(jsonPayload), "application/json"); + } + request.method(requestMethod.getHttpMethod()); + + try { + if (agentStartupConfiguration.isKeycloakEnabled()) { + KeycloakAccessToken accessToken = tokenService.getAccessToken(); + request.header(HttpHeader.AUTHORIZATION.asString(), "Bearer " + accessToken.getAccessToken()); + } else if (agentStartupConfiguration.isBasicAuthEnabled()) { + request.header(HttpHeader.AUTHORIZATION.asString(), + getBasicAuthHeaderValue()); + } else { + logger.warning("Neither KEYCLOAK_ENABLED=true nor BASIC_AUTH_ENABLED=true. Requests will probably fail."); + } + ContentResponse response = request.send(); + int status = response.getStatus(); + if (status != HttpStatus.OK_200) { + throw new RequestFailedException(status, "Request to gateway failed. Reason: " + response.getReason()); + } + if (requestMethod == Method.GET) { + return response.getContentAsString(); + } else { + return null; + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new RequestFailedException(e); + } + } + + private String getBasicAuthHeaderValue() { + String username = creds.getUsername(); + char[] pwdChar = creds.getPassword(); + String userpassword; + if (username == null || username.isEmpty() || pwdChar == null) { + logger.warning("No credentials specified in " + commonPaths.getUserAgentAuthConfigFile() + ". The connection will fail."); + userpassword = UNKNOWN_CREDS; + } else { + String pwd = new String(pwdChar); + userpassword = username + ":" + pwd; + } + + @SuppressWarnings("restriction") + String encodedAuthorization = new sun.misc.BASE64Encoder() + .encode(userpassword.getBytes()); + return "Basic " + encodedAuthorization; + } + + // DS bind methods + + protected void bindTokenService(KeycloakAccessTokenService tokenService) { + this.tokenService = tokenService; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/internal/http/KeycloakAccessTokenServiceImpl.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,168 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.internal.http; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.redhat.thermostat.agent.http.RequestFailedException; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessToken; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessTokenService; +import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.shared.config.CommonPaths; +import com.redhat.thermostat.shared.config.SSLConfiguration; + +@Component +@Service(value = KeycloakAccessTokenService.class) +public class KeycloakAccessTokenServiceImpl extends BasicHttpService implements KeycloakAccessTokenService { + + private static final Logger logger = LoggingUtils.getLogger(KeycloakAccessTokenServiceImpl.class); + private static final String KEYCLOAK_TOKEN_SERVICE = "/auth/realms/__REALM__/protocol/openid-connect/token"; + private static final String KEYCLOAK_CONTENT_TYPE = "application/x-www-form-urlencoded"; + private final Object accessTokenLock = new Object(); + private final Gson gson = new GsonBuilder().create(); + private KeycloakAccessToken keycloakAccessToken; + + @Reference + private SSLConfiguration sslConfig; + @Reference + private CommonPaths commonPaths; + + public KeycloakAccessTokenServiceImpl() { + this(new HttpClientCreator(), new ConfigCreator(), new CredentialsCreator()); + } + + // For testing purposes + KeycloakAccessTokenServiceImpl(HttpClientCreator clientCreator, ConfigCreator configCreator, CredentialsCreator credsCreator) { + super(clientCreator, configCreator, credsCreator); + } + + @Activate + public void activate() { + super.doActivate(commonPaths, sslConfig); + } + + @Override + public KeycloakAccessToken getAccessToken() throws RequestFailedException { + synchronized(accessTokenLock) { + if (keycloakAccessToken == null) { + keycloakAccessToken = acquireKeycloakToken(); + } else if (keycloakAccessToken.isKeycloakTokenExpired()) { + logger.log(Level.FINE, "Keycloak Token expired attempting to reacquire via refresh_token"); + keycloakAccessToken = refreshKeycloakToken(); + + if (keycloakAccessToken == null) { + logger.log(Level.WARNING, "Unable to refresh Keycloak token, attempting to acquire new token"); + keycloakAccessToken = acquireKeycloakToken(); + } + + if (keycloakAccessToken == null) { + logger.log(Level.SEVERE, "Unable to reacquire KeycloakToken."); + throw new RequestFailedException("Keycloak token expired and attempt to refresh and reacquire Keycloak token failed."); + } + } + + } + return keycloakAccessToken; + } + + private KeycloakAccessToken acquireKeycloakToken() { + return requestKeycloakToken(getKeycloakAccessPayload()); + } + + private KeycloakAccessToken refreshKeycloakToken() { + return requestKeycloakToken(getKeycloakRefreshPayload()); + } + + private KeycloakAccessToken requestKeycloakToken(String payload) { + String url = agentStartupConfiguration.getKeycloakUrl() + KEYCLOAK_TOKEN_SERVICE.replace("__REALM__", agentStartupConfiguration.getKeycloakRealm()); + Request request = client.newRequest(url); + request.content(new StringContentProvider(payload), KEYCLOAK_CONTENT_TYPE); + request.method(HttpMethod.POST); + + try { + ContentResponse response = request.send(); + if (response.getStatus() == HttpStatus.OK_200) { + + String content = response.getContentAsString(); + + keycloakAccessToken = gson.fromJson(content, KeycloakAccessToken.class); + keycloakAccessToken.setAcquireTime(System.nanoTime()); + + logger.log(Level.FINE, "Keycloak Token acquired"); + return keycloakAccessToken; + } else { + logger.log(Level.WARNING, "Failed to acquire Keycloak token: " + response.getStatus() + " " + response.getReason()); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to acquire Keycloak token", e); + } + return null; + } + + private String getKeycloakAccessPayload() { + String username = creds.getUsername(); + String password = new String(creds.getPassword()); + return "grant_type=password&client_id=" + agentStartupConfiguration.getKeycloakClient() + + "&username=" + username + + "&password=" + password; + } + + private String getKeycloakRefreshPayload() { + return "grant_type=refresh_token&client_id=" + agentStartupConfiguration.getKeycloakClient() + + "&refresh_token=" + keycloakAccessToken.getRefreshToken(); + } + + // For testing purpuses + void setKeycloakAccessToken(KeycloakAccessToken token) { + synchronized (accessTokenLock) { + this.keycloakAccessToken = token; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/keycloak/KeycloakAccessToken.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.keycloak; + +import java.util.concurrent.TimeUnit; + +import com.google.gson.annotations.SerializedName; + +public class KeycloakAccessToken { + + @SerializedName("access_token") + private String accessToken; + + // In seconds + @SerializedName("expires_in") + private long expiresIn; + + // In seconds + @SerializedName("refresh_expires_in") + private long refreshExpiresIn; + + @SerializedName("refresh_token") + private String refreshToken; + + // In nanoseconds + private transient long acquireTime; + + public KeycloakAccessToken() { + // default constructor + } + + // for testing + KeycloakAccessToken(Long expiresIn) { + this.expiresIn = expiresIn; + } + + public String getAccessToken() { + return accessToken; + } + + public Long getExpiresIn() { + return expiresIn; + } + + public Long getRefreshExpiresIn() { + return refreshExpiresIn; + } + + public String getRefreshToken() { + return refreshToken; + } + + public long getAcquireTime() { + return acquireTime; + } + + public void setAcquireTime(long acquireTime) { + this.acquireTime = acquireTime; + } + + public boolean isKeycloakTokenExpired() { + return System.nanoTime() > TimeUnit.NANOSECONDS.convert(getExpiresIn(), TimeUnit.SECONDS) + getAcquireTime(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/keycloak/KeycloakAccessTokenService.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.keycloak; + +import com.redhat.thermostat.agent.http.RequestFailedException; +import com.redhat.thermostat.annotations.Service; + +/** + * Service for retrieving a Keycloak access token. + */ +@Service +public interface KeycloakAccessTokenService { + + KeycloakAccessToken getAccessToken() throws RequestFailedException; + +}
--- a/agent/core/src/test/java/com/redhat/thermostat/agent/http/HttpRequestServiceTest.java Thu Sep 07 18:41:40 2017 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,379 +0,0 @@ -/* - * Copyright 2012-2017 Red Hat, Inc. - * - * This file is part of Thermostat. - * - * Thermostat is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published - * by the Free Software Foundation; either version 2, or (at your - * option) any later version. - * - * Thermostat is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Thermostat; see the file COPYING. If not see - * <http://www.gnu.org/licenses/>. - * - * Linking this code with other modules is making a combined work - * based on this code. Thus, the terms and conditions of the GNU - * General Public License cover the whole combination. - * - * As a special exception, the copyright holders of this code give - * you permission to link this code with independent modules to - * produce an executable, regardless of the license terms of these - * independent modules, and to copy and distribute the resulting - * executable under terms of your choice, provided that you also - * meet, for each linked independent module, the terms and conditions - * of the license of that module. An independent module is a module - * which is not derived from or based on this code. If you modify - * this code, you may extend this exception to your version of the - * library, but you are not obligated to do so. If you do not wish - * to do so, delete this exception statement from your version. - */ - -package com.redhat.thermostat.agent.http; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpStatus; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import com.redhat.thermostat.agent.config.AgentStartupConfiguration; -import com.redhat.thermostat.agent.http.HttpRequestService.ConfigCreator; -import com.redhat.thermostat.agent.http.HttpRequestService.CredentialsCreator; -import com.redhat.thermostat.agent.http.HttpRequestService.HttpClientCreator; -import com.redhat.thermostat.agent.http.HttpRequestService.RequestFailedException; -import com.redhat.thermostat.shared.config.CommonPaths; -import com.redhat.thermostat.shared.config.SSLConfiguration; -import com.redhat.thermostat.storage.core.StorageCredentials; - -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; - -public class HttpRequestServiceTest { - private static final URI GATEWAY_URI = URI.create("http://127.0.0.1:30000/test/"); - private static final URI GET_URI = GATEWAY_URI.resolve("?q=foo&l=3"); - private static final String payload = "{}"; - private static final String keycloakUrl = "http://127.0.0.1:31000/keycloak"; - private static final char[] PASSWORD = new char[] { 'p', 'a', 's', 's' }; - private static final String USERNAME = "testing"; - - private HttpClientCreator clientCreator; - private ConfigCreator configCreator; - private CredentialsCreator credsCreator; - private HttpClientFacade client; - private Request httpRequest; - - @Before - public void setup() throws InterruptedException, ExecutionException, TimeoutException { - client = mock(HttpClientFacade.class); - httpRequest = mock(Request.class); - when(client.newRequest(eq(GATEWAY_URI))).thenReturn(httpRequest); - ContentResponse response = mock(ContentResponse.class); - when(response.getStatus()).thenReturn(HttpStatus.OK_200); - when(httpRequest.send()).thenReturn(response); - clientCreator = mock(HttpClientCreator.class); - when(clientCreator.create(any(SSLConfiguration.class))).thenReturn(client); - configCreator = mock(ConfigCreator.class); - credsCreator = mock(CredentialsCreator.class); - StorageCredentials creds = mock(StorageCredentials.class); - when(creds.getPassword()).thenReturn(PASSWORD); - when(creds.getUsername()).thenReturn(USERNAME); - when(credsCreator.create(any(CommonPaths.class))).thenReturn(creds); - } - - @Test - public void testRequestWithoutKeycloak() throws Exception { - AgentStartupConfiguration configuration = createBasicAuthConfig(); - - HttpRequestService service = createAndActivateRequestService(configuration); - - service.sendHttpRequest(payload, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); - - verify(configuration).isKeycloakEnabled(); - verifyHttpPostRequest(httpRequest); - } - - @Test - public void testRequestWithKeycloak() throws Exception { - AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class); - setupKeycloakConfig(configuration); - - HttpRequestService service = createAndActivateRequestService(configuration); - - Request keycloakRequest = mock(Request.class); - setupKeycloakRequest(keycloakRequest); - - service.sendHttpRequest(payload, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); - - verify(configuration).isKeycloakEnabled(); - verifyHttpPostRequest(httpRequest); - - verify(httpRequest).header(eq("Authorization"), eq("Bearer access")); - verifyKeycloakAcquire(keycloakRequest); - } - - @Test - public void testRequestWithKeycloakRefresh() throws Exception { - AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class); - setupKeycloakConfig(configuration); - - HttpRequestService service = createAndActivateRequestService(configuration); - - Request keycloakRequest = mock(Request.class); - setupKeycloakRequest(keycloakRequest); - - service.sendHttpRequest(payload, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); - - verify(configuration).isKeycloakEnabled(); - verifyHttpPostRequest(httpRequest); - - verify(httpRequest).header(eq("Authorization"), eq("Bearer access")); - - verifyKeycloakAcquire(keycloakRequest); - - service.sendHttpRequest(payload, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); - - - ArgumentCaptor<StringContentProvider> payloadCaptor = ArgumentCaptor.forClass(StringContentProvider.class); - verify(keycloakRequest, times(2)).content(payloadCaptor.capture(), eq("application/x-www-form-urlencoded")); - verify(keycloakRequest, times(2)).method(eq(HttpMethod.POST)); - verify(keycloakRequest, times(2)).send(); - - String expected = "grant_type=refresh_token&client_id=client&refresh_token=refresh"; - - StringContentProvider provider = payloadCaptor.getValue(); - for (ByteBuffer buffer : provider) { - byte[] bytes = buffer.array(); - String content = new String(bytes, StandardCharsets.UTF_8); - assertEquals(expected, content); - } - } - - @Test - public void testRequestWithNullPayload() throws Exception { - AgentStartupConfiguration configuration = createBasicAuthConfig(); - - HttpRequestService service = createAndActivateRequestService(configuration); - - String response = service.sendHttpRequest(null, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); - assertNull(response); - - verify(client).newRequest(GATEWAY_URI); - verify(configuration).isKeycloakEnabled(); - - verify(httpRequest, times(0)).content(any(StringContentProvider.class), anyString()); - verify(httpRequest).method(eq(HttpMethod.POST)); - verify(httpRequest).send(); - } - - @Test - public void verifyBasicAuthConfig() throws Exception { - AgentStartupConfiguration configuration = createBasicAuthConfig(); - - HttpRequestService service = createAndActivateRequestService(configuration); - - service.sendHttpRequest(null, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); - - verify(client).newRequest(GATEWAY_URI); - verify(configuration).isKeycloakEnabled(); - - ArgumentCaptor<String> authValueCaptor = ArgumentCaptor.forClass(String.class); - verify(httpRequest).header(eq(HttpHeader.AUTHORIZATION.asString()), authValueCaptor.capture()); - String authValueEncoded = authValueCaptor.getValue(); - assertTrue(authValueEncoded.startsWith("Basic ")); - String userPassEncoded = authValueEncoded.substring("Basic ".length()); - String decodedUserPass = getDecodedUserPass(userPassEncoded); - String expectedCreds = USERNAME + ":" + new String(PASSWORD); - assertEquals(expectedCreds, decodedUserPass); - verify(httpRequest).method(eq(HttpMethod.GET)); - verify(httpRequest).send(); - } - - /** - * If no authentication settings are done, no authorization headers should - * get added. - * - * @throws Exception - */ - @Test - public void verifyNoAuthConfig() throws Exception { - AgentStartupConfiguration configuration = createNoAuthConfig(); - - HttpRequestService service = createAndActivateRequestService(configuration); - - service.sendHttpRequest(null, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); - - verify(client).newRequest(GATEWAY_URI); - verify(configuration).isKeycloakEnabled(); - verify(configuration).isBasicAuthEnabled(); - - verify(httpRequest, times(0)).header(eq(HttpHeader.AUTHORIZATION.asString()), anyString()); - verify(httpRequest).method(eq(HttpMethod.GET)); - verify(httpRequest).send(); - } - - private String getDecodedUserPass(String userPassEncoded) throws IOException { - @SuppressWarnings("restriction") - byte[] decodedBytes = new sun.misc.BASE64Decoder().decodeBuffer(userPassEncoded); - return new String(decodedBytes); - } - - private HttpRequestService createAndActivateRequestService(AgentStartupConfiguration configuration) throws Exception { - when(configCreator.create(any(CommonPaths.class))).thenReturn(configuration); - HttpRequestService service = new HttpRequestService(clientCreator, configCreator, credsCreator); - service.activate(); - verify(client).start(); - return service; - } - - @Test - public void testGetRequestWithResponse() throws Exception { - String getContent = "foo bar"; - HttpClientCreator creator = mock(HttpClientCreator.class); - HttpClientFacade getClient = setupHttpClient(creator, getContent); - - AgentStartupConfiguration configuration = createBasicAuthConfig(); - ConfigCreator configCreator = mock(ConfigCreator.class); - when(configCreator.create(any(CommonPaths.class))).thenReturn(configuration); - HttpRequestService service = new HttpRequestService(creator, configCreator, credsCreator); - service.activate(); - String content = service.sendHttpRequest(null, GET_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); - verify(getClient).newRequest(GET_URI); - assertEquals(getContent, content); - } - - @Test - public void testGetRequestNormalizesURI() throws Exception { - String getContent = "foo bar"; - HttpClientCreator creator = mock(HttpClientCreator.class); - HttpClientFacade getClient = setupHttpClient(creator, getContent); - - AgentStartupConfiguration configuration = createBasicAuthConfig(); - ConfigCreator configCreator = mock(ConfigCreator.class); - when(configCreator.create(any(CommonPaths.class))).thenReturn(configuration); - HttpRequestService service = new HttpRequestService(creator, configCreator, credsCreator); - service.activate(); - - // Add extra slashes to URI - URI uri = URI.create("http://127.0.0.1:30000//test//?q=bar&l=5"); - URI normalized = URI.create("http://127.0.0.1:30000/test/?q=bar&l=5"); - String content = service.sendHttpRequest(null, uri, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); - verify(getClient).newRequest(normalized); - assertEquals(getContent, content); - } - - private HttpClientFacade setupHttpClient(HttpClientCreator creator, String getContent) throws Exception { - Request request = mock(Request.class); - ContentResponse contentResponse = mock(ContentResponse.class); - when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200); - when(contentResponse.getContentAsString()).thenReturn(getContent); - when(request.send()).thenReturn(contentResponse); - HttpClientFacade getClient = mock(HttpClientFacade.class); - when(getClient.newRequest(any(URI.class))).thenReturn(request); - when(creator.create(any(SSLConfiguration.class))).thenReturn(getClient); - return getClient; - } - - @Test(expected = RequestFailedException.class) - public void failureThrowsRequestFailedException() throws Exception { - Request request = mock(Request.class); - when(client.newRequest(any(URI.class))).thenReturn(request); - AgentStartupConfiguration configuration = createBasicAuthConfig(); - doThrow(IOException.class).when(request).send(); - HttpRequestService service = createAndActivateRequestService(configuration); - service.sendHttpRequest("foo", GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.DELETE /*any valid method*/); - } - - private AgentStartupConfiguration createBasicAuthConfig() { - return createAuthConfig(true, false); - } - - private AgentStartupConfiguration createNoAuthConfig() { - return createAuthConfig(false, false); - } - - private AgentStartupConfiguration createAuthConfig(boolean isBasicAuthEnabled, boolean isKeycloakEnabled) { - AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class); - when(configuration.isKeycloakEnabled()).thenReturn(isKeycloakEnabled); - when(configuration.isBasicAuthEnabled()).thenReturn(isBasicAuthEnabled); - return configuration; - } - - private void verifyHttpPostRequest(Request httpRequest) throws InterruptedException, ExecutionException, TimeoutException { - verify(client).newRequest(GATEWAY_URI); - verify(httpRequest).content(any(StringContentProvider.class), eq("application/json")); - verify(httpRequest).method(eq(HttpMethod.POST)); - verify(httpRequest).send(); - } - - private void setupKeycloakConfig(AgentStartupConfiguration configuration) { - when(configuration.isKeycloakEnabled()).thenReturn(true); - when(configuration.getKeycloakUrl()).thenReturn(keycloakUrl); - when(configuration.getKeycloakClient()).thenReturn("client"); - when(configuration.getKeycloakRealm()).thenReturn("realm"); - } - - private void setupKeycloakRequest(Request keycloakRequest) throws InterruptedException, ExecutionException, TimeoutException { - when(client.newRequest(keycloakUrl + "/auth/realms/realm/protocol/openid-connect/token")).thenReturn(keycloakRequest); - - ContentResponse response = mock(ContentResponse.class); - when(response.getStatus()).thenReturn(HttpStatus.OK_200); - - String keycloakToken = "{" + - "\"access_token\": \"access\"," + - "\"expires_in\": 0," + - "\"refresh_expires_in\": 2," + - "\"refresh_token\": \"refresh\"," + - "\"token_type\": \"bearer\"," + - "\"id_token\": \"id\"," + - "\"not-before-policy\": 3," + - "\"session_state\": \"state\"" + - "}"; - - when(response.getContentAsString()).thenReturn(keycloakToken); - when(keycloakRequest.send()).thenReturn(response); - } - - private void verifyKeycloakAcquire(Request keycloakRequest) throws InterruptedException, ExecutionException, TimeoutException { - ArgumentCaptor<StringContentProvider> payloadCaptor = ArgumentCaptor.forClass(StringContentProvider.class); - verify(keycloakRequest).content(payloadCaptor.capture(), eq("application/x-www-form-urlencoded")); - verify(keycloakRequest).method(eq(HttpMethod.POST)); - verify(keycloakRequest).send(); - - String expected = "grant_type=password&client_id=client&username=" + USERNAME + "&password=" + new String(PASSWORD); - - StringContentProvider provider = payloadCaptor.getValue(); - for (ByteBuffer buffer : provider) { - byte[] bytes = buffer.array(); - String content = new String(bytes, StandardCharsets.UTF_8); - assertEquals(expected, content); - } - - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/test/java/com/redhat/thermostat/agent/internal/http/HttpRequestServiceImplTest.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,301 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.internal.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import com.redhat.thermostat.agent.config.AgentStartupConfiguration; +import com.redhat.thermostat.agent.http.HttpRequestService; +import com.redhat.thermostat.agent.http.RequestFailedException; +import com.redhat.thermostat.agent.internal.http.BasicHttpService.ConfigCreator; +import com.redhat.thermostat.agent.internal.http.BasicHttpService.CredentialsCreator; +import com.redhat.thermostat.agent.internal.http.BasicHttpService.HttpClientCreator; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessToken; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessTokenService; +import com.redhat.thermostat.shared.config.CommonPaths; +import com.redhat.thermostat.shared.config.SSLConfiguration; +import com.redhat.thermostat.storage.core.StorageCredentials; + +public class HttpRequestServiceImplTest { + private static final URI GATEWAY_URI = URI.create("http://127.0.0.1:30000/test/"); + private static final URI GET_URI = GATEWAY_URI.resolve("?q=foo&l=3"); + private static final String payload = "{}"; + private static final char[] PASSWORD = new char[] { 'p', 'a', 's', 's' }; + private static final String USERNAME = "testing"; + private static final String KEYCLOAK_ACCESS_TOKEN_STRING = "keycloak-bearer-token-value"; + + private HttpClientCreator clientCreator; + private ConfigCreator configCreator; + private CredentialsCreator credsCreator; + private HttpClientFacade client; + private Request httpRequest; + private KeycloakAccessTokenService tokenService; + + @Before + public void setup() throws InterruptedException, ExecutionException, TimeoutException, RequestFailedException { + client = mock(HttpClientFacade.class); + httpRequest = mock(Request.class); + when(client.newRequest(eq(GATEWAY_URI))).thenReturn(httpRequest); + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(HttpStatus.OK_200); + when(httpRequest.send()).thenReturn(response); + clientCreator = mock(HttpClientCreator.class); + when(clientCreator.create(any(SSLConfiguration.class))).thenReturn(client); + configCreator = mock(ConfigCreator.class); + credsCreator = mock(CredentialsCreator.class); + StorageCredentials creds = mock(StorageCredentials.class); + when(creds.getPassword()).thenReturn(PASSWORD); + when(creds.getUsername()).thenReturn(USERNAME); + when(credsCreator.create(any(CommonPaths.class))).thenReturn(creds); + tokenService = mock(KeycloakAccessTokenService.class); + KeycloakAccessToken keycloakAccessToken = mock(KeycloakAccessToken.class); + when(keycloakAccessToken.getAccessToken()).thenReturn(KEYCLOAK_ACCESS_TOKEN_STRING); + when(tokenService.getAccessToken()).thenReturn(keycloakAccessToken); + } + + @Test + public void testRequestWithBasicAuth() throws Exception { + AgentStartupConfiguration configuration = createBasicAuthConfig(); + + HttpRequestService service = createAndActivateRequestService(configuration); + + service.sendHttpRequest(payload, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); + + verify(configuration).isKeycloakEnabled(); + verifyHttpPostRequest(httpRequest); + verify(tokenService, times(0)).getAccessToken(); + } + + @Test + public void testRequestWithKeycloakAuth() throws Exception { + AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class); + when(configuration.isKeycloakEnabled()).thenReturn(true); + + HttpRequestService service = createAndActivateRequestService(configuration); + + service.sendHttpRequest(payload, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); + + verify(configuration).isKeycloakEnabled(); + verifyHttpPostRequest(httpRequest); + + verify(httpRequest).header(eq("Authorization"), eq("Bearer " + KEYCLOAK_ACCESS_TOKEN_STRING)); + verify(tokenService).getAccessToken(); + } + + @Test + public void testRequestWithNullPayload() throws Exception { + AgentStartupConfiguration configuration = createBasicAuthConfig(); + + HttpRequestService service = createAndActivateRequestService(configuration); + + String response = service.sendHttpRequest(null, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.POST); + assertNull(response); + + verify(client).newRequest(GATEWAY_URI); + verify(configuration).isKeycloakEnabled(); + + verify(httpRequest, times(0)).content(any(StringContentProvider.class), anyString()); + verify(httpRequest).method(eq(HttpMethod.POST)); + verify(httpRequest).send(); + } + + @Test + public void verifyBasicAuthConfig() throws Exception { + AgentStartupConfiguration configuration = createBasicAuthConfig(); + + HttpRequestService service = createAndActivateRequestService(configuration); + + service.sendHttpRequest(null, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); + + verify(client).newRequest(GATEWAY_URI); + verify(configuration).isKeycloakEnabled(); + + ArgumentCaptor<String> authValueCaptor = ArgumentCaptor.forClass(String.class); + verify(httpRequest).header(eq(HttpHeader.AUTHORIZATION.asString()), authValueCaptor.capture()); + String authValueEncoded = authValueCaptor.getValue(); + assertTrue(authValueEncoded.startsWith("Basic ")); + String userPassEncoded = authValueEncoded.substring("Basic ".length()); + String decodedUserPass = getDecodedUserPass(userPassEncoded); + String expectedCreds = USERNAME + ":" + new String(PASSWORD); + assertEquals(expectedCreds, decodedUserPass); + verify(httpRequest).method(eq(HttpMethod.GET)); + verify(httpRequest).send(); + } + + /** + * If no authentication settings are done, no authorization headers should + * get added. + * + * @throws Exception + */ + @Test + public void verifyNoAuthConfig() throws Exception { + AgentStartupConfiguration configuration = createNoAuthConfig(); + + HttpRequestService service = createAndActivateRequestService(configuration); + + service.sendHttpRequest(null, GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); + + verify(client).newRequest(GATEWAY_URI); + verify(configuration).isKeycloakEnabled(); + verify(configuration).isBasicAuthEnabled(); + + verify(httpRequest, times(0)).header(eq(HttpHeader.AUTHORIZATION.asString()), anyString()); + verify(httpRequest).method(eq(HttpMethod.GET)); + verify(httpRequest).send(); + } + + private String getDecodedUserPass(String userPassEncoded) throws IOException { + @SuppressWarnings("restriction") + byte[] decodedBytes = new sun.misc.BASE64Decoder().decodeBuffer(userPassEncoded); + return new String(decodedBytes); + } + + private HttpRequestService createAndActivateRequestService(AgentStartupConfiguration configuration) throws Exception { + when(configCreator.create(any(CommonPaths.class))).thenReturn(configuration); + HttpRequestServiceImpl service = new HttpRequestServiceImpl(clientCreator, configCreator, credsCreator); + service.bindTokenService(tokenService); + service.activate(); + verify(client).start(); + return service; + } + + @Test + public void testGetRequestWithResponse() throws Exception { + String getContent = "foo bar"; + HttpClientCreator creator = mock(HttpClientCreator.class); + HttpClientFacade getClient = setupHttpClient(creator, getContent); + + AgentStartupConfiguration configuration = createBasicAuthConfig(); + ConfigCreator configCreator = mock(ConfigCreator.class); + when(configCreator.create(any(CommonPaths.class))).thenReturn(configuration); + HttpRequestServiceImpl service = new HttpRequestServiceImpl(creator, configCreator, credsCreator); + service.activate(); + String content = service.sendHttpRequest(null, GET_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); + verify(getClient).newRequest(GET_URI); + assertEquals(getContent, content); + } + + @Test + public void testGetRequestNormalizesURI() throws Exception { + String getContent = "foo bar"; + HttpClientCreator creator = mock(HttpClientCreator.class); + HttpClientFacade getClient = setupHttpClient(creator, getContent); + + AgentStartupConfiguration configuration = createBasicAuthConfig(); + ConfigCreator configCreator = mock(ConfigCreator.class); + when(configCreator.create(any(CommonPaths.class))).thenReturn(configuration); + HttpRequestServiceImpl service = new HttpRequestServiceImpl(creator, configCreator, credsCreator); + service.activate(); + + // Add extra slashes to URI + URI uri = URI.create("http://127.0.0.1:30000//test//?q=bar&l=5"); + URI normalized = URI.create("http://127.0.0.1:30000/test/?q=bar&l=5"); + String content = service.sendHttpRequest(null, uri, com.redhat.thermostat.agent.http.HttpRequestService.Method.GET); + verify(getClient).newRequest(normalized); + assertEquals(getContent, content); + } + + private HttpClientFacade setupHttpClient(HttpClientCreator creator, String getContent) throws Exception { + Request request = mock(Request.class); + ContentResponse contentResponse = mock(ContentResponse.class); + when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200); + when(contentResponse.getContentAsString()).thenReturn(getContent); + when(request.send()).thenReturn(contentResponse); + HttpClientFacade getClient = mock(HttpClientFacade.class); + when(getClient.newRequest(any(URI.class))).thenReturn(request); + when(creator.create(any(SSLConfiguration.class))).thenReturn(getClient); + return getClient; + } + + @Test(expected = RequestFailedException.class) + public void failureThrowsRequestFailedException() throws Exception { + Request request = mock(Request.class); + when(client.newRequest(any(URI.class))).thenReturn(request); + AgentStartupConfiguration configuration = createBasicAuthConfig(); + doThrow(RequestFailedException.class).when(request).send(); + HttpRequestService service = createAndActivateRequestService(configuration); + service.sendHttpRequest("foo", GATEWAY_URI, com.redhat.thermostat.agent.http.HttpRequestService.Method.DELETE /*any valid method*/); + } + + private AgentStartupConfiguration createBasicAuthConfig() { + return createAuthConfig(true, false); + } + + private AgentStartupConfiguration createNoAuthConfig() { + return createAuthConfig(false, false); + } + + private AgentStartupConfiguration createAuthConfig(boolean isBasicAuthEnabled, boolean isKeycloakEnabled) { + AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class); + when(configuration.isKeycloakEnabled()).thenReturn(isKeycloakEnabled); + when(configuration.isBasicAuthEnabled()).thenReturn(isBasicAuthEnabled); + return configuration; + } + + private void verifyHttpPostRequest(Request httpRequest) throws InterruptedException, ExecutionException, TimeoutException { + verify(client).newRequest(GATEWAY_URI); + verify(httpRequest).content(any(StringContentProvider.class), eq("application/json")); + verify(httpRequest).method(eq(HttpMethod.POST)); + verify(httpRequest).send(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/test/java/com/redhat/thermostat/agent/internal/http/KeycloakAccessTokenServiceImplTest.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,189 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.internal.http; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import com.redhat.thermostat.agent.config.AgentStartupConfiguration; +import com.redhat.thermostat.agent.http.RequestFailedException; +import com.redhat.thermostat.agent.internal.http.BasicHttpService.ConfigCreator; +import com.redhat.thermostat.agent.internal.http.BasicHttpService.CredentialsCreator; +import com.redhat.thermostat.agent.internal.http.BasicHttpService.HttpClientCreator; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessToken; +import com.redhat.thermostat.agent.keycloak.KeycloakAccessTokenService; +import com.redhat.thermostat.shared.config.CommonPaths; +import com.redhat.thermostat.shared.config.SSLConfiguration; +import com.redhat.thermostat.storage.core.StorageCredentials; + +public class KeycloakAccessTokenServiceImplTest { + + private static final String KEYCLOAK_URL = "http://127.0.0.1:31000/keycloak"; + private static final String KEYCLOAK_CLIENT = "client"; + private static final String KEYCLOAK_REALM = "realm"; + private static final char[] PASSWORD = new char[] { 'p', 'a', 's', 's' }; + private static final String USERNAME = "testing"; + + private HttpClientCreator clientCreator; + private ConfigCreator configCreator; + private CredentialsCreator credsCreator; + private HttpClientFacade client; + private AgentStartupConfiguration configuration; + private Request keycloakRequest = mock(Request.class); + + @Before + public void setup() throws InterruptedException, ExecutionException, TimeoutException, RequestFailedException { + client = mock(HttpClientFacade.class); + clientCreator = mock(HttpClientCreator.class); + when(clientCreator.create(any(SSLConfiguration.class))).thenReturn(client); + configCreator = mock(ConfigCreator.class); + credsCreator = mock(CredentialsCreator.class); + StorageCredentials creds = mock(StorageCredentials.class); + when(creds.getPassword()).thenReturn(PASSWORD); + when(creds.getUsername()).thenReturn(USERNAME); + when(credsCreator.create(any(CommonPaths.class))).thenReturn(creds); + configuration = mock(AgentStartupConfiguration.class); + setupKeycloakConfig(configuration); + keycloakRequest = mock(Request.class); + setupKeycloakRequest(keycloakRequest); + } + + @Test + public void verifyGetAccessToken() throws Exception { + KeycloakAccessTokenService service = createAndActivateRequestService(); + + service.getAccessToken(); + + verifyKeycloakAcquire(); + } + + @Test + public void verifyKeycloakAccessTokenRefresh() throws Exception { + String refreshTokenValue = "refresh"; + KeycloakAccessToken expiredToken = mock(KeycloakAccessToken.class); + when(expiredToken.isKeycloakTokenExpired()).thenReturn(true); + when(expiredToken.getRefreshToken()).thenReturn(refreshTokenValue); + KeycloakAccessTokenService service = createAndActivateRequestService(expiredToken); + + service.getAccessToken(); + + verifyKeycloakRefresh(refreshTokenValue); + } + + private KeycloakAccessTokenService createAndActivateRequestService() throws Exception { + return createAndActivateRequestService(null); + } + + private KeycloakAccessTokenService createAndActivateRequestService(KeycloakAccessToken accessTokenInitial) throws Exception { + when(configCreator.create(any(CommonPaths.class))).thenReturn(configuration); + KeycloakAccessTokenServiceImpl service = new KeycloakAccessTokenServiceImpl(clientCreator, configCreator, credsCreator); + service.setKeycloakAccessToken(accessTokenInitial); + service.activate(); + verify(client).start(); + return service; + } + + private void setupKeycloakConfig(AgentStartupConfiguration configuration) { + when(configuration.isKeycloakEnabled()).thenReturn(true); + when(configuration.getKeycloakUrl()).thenReturn(KEYCLOAK_URL); + when(configuration.getKeycloakClient()).thenReturn(KEYCLOAK_CLIENT); + when(configuration.getKeycloakRealm()).thenReturn(KEYCLOAK_REALM); + } + + private void setupKeycloakRequest(Request keycloakRequest) throws InterruptedException, ExecutionException, TimeoutException { + when(client.newRequest(KEYCLOAK_URL + "/auth/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token")).thenReturn(keycloakRequest); + + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(HttpStatus.OK_200); + + String keycloakToken = "{" + + "\"access_token\": \"access\"," + + "\"expires_in\": 0," + + "\"refresh_expires_in\": 2," + + "\"refresh_token\": \"refresh\"," + + "\"token_type\": \"bearer\"," + + "\"id_token\": \"id\"," + + "\"not-before-policy\": 3," + + "\"session_state\": \"state\"" + + "}"; + + when(response.getContentAsString()).thenReturn(keycloakToken); + when(keycloakRequest.send()).thenReturn(response); + } + + private void verifyKeycloakAcquire() throws InterruptedException, ExecutionException, TimeoutException { + String expected = "grant_type=password&client_id=" + KEYCLOAK_CLIENT + "&username=" + USERNAME + "&password=" + new String(PASSWORD); + doKeycloakVerification(expected); + } + + private void verifyKeycloakRefresh(String refreshTokenValue) throws InterruptedException, TimeoutException, ExecutionException { + String expected = "grant_type=refresh_token&client_id=" + KEYCLOAK_CLIENT + "&refresh_token=" + refreshTokenValue; + doKeycloakVerification(expected); + } + + private void doKeycloakVerification(final String expectedPayload) throws InterruptedException, TimeoutException, ExecutionException { + ArgumentCaptor<StringContentProvider> payloadCaptor = ArgumentCaptor.forClass(StringContentProvider.class); + verify(keycloakRequest).content(payloadCaptor.capture(), eq("application/x-www-form-urlencoded")); + verify(keycloakRequest).method(eq(HttpMethod.POST)); + verify(keycloakRequest).send(); + + StringContentProvider provider = payloadCaptor.getValue(); + for (ByteBuffer buffer : provider) { + byte[] bytes = buffer.array(); + String content = new String(bytes, StandardCharsets.UTF_8); + assertEquals(expectedPayload, content); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/test/java/com/redhat/thermostat/agent/keycloak/KeycloakAccessTokenTest.java Wed Sep 06 19:47:49 2017 +0200 @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.agent.keycloak; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; +import org.junit.Test; + +public class KeycloakAccessTokenTest { + + private static final int MINUTES = 60; + + @Test + public void verifyExpiredToken() { + Long expiresIn = 3L; + Long acquireTime = 3L; + KeycloakAccessToken token = new KeycloakAccessToken(expiresIn); + token.setAcquireTime(acquireTime); + assumeTrue(System.nanoTime() > TimeUnit.NANOSECONDS.convert(expiresIn, TimeUnit.SECONDS) + acquireTime); + assertEquals("token is expired", true, token.isKeycloakTokenExpired()); + } + + @Test + public void verifyValidToken() { + Long expiresIn = 10L * MINUTES; + Long acquireTime = System.nanoTime(); + KeycloakAccessToken token = new KeycloakAccessToken(expiresIn); + token.setAcquireTime(acquireTime); + assumeTrue(System.nanoTime() <= TimeUnit.NANOSECONDS.convert(expiresIn, TimeUnit.SECONDS) + acquireTime); + assertEquals("token should still be valid", false, token.isKeycloakTokenExpired()); + } +}
--- a/common/plugin/src/main/java/com/redhat/thermostat/common/plugin/PluginDAOBase.java Thu Sep 07 18:41:40 2017 +0200 +++ b/common/plugin/src/main/java/com/redhat/thermostat/common/plugin/PluginDAOBase.java Wed Sep 06 19:47:49 2017 +0200 @@ -42,7 +42,7 @@ import java.util.logging.Logger; import com.redhat.thermostat.agent.http.HttpRequestService; -import com.redhat.thermostat.agent.http.HttpRequestService.RequestFailedException; +import com.redhat.thermostat.agent.http.RequestFailedException; abstract public class PluginDAOBase<Tobj> {
--- a/plugins/jvm-overview/agent/src/main/java/com/redhat/thermostat/jvm/overview/agent/internal/model/VmInfoDAOImpl.java Thu Sep 07 18:41:40 2017 +0200 +++ b/plugins/jvm-overview/agent/src/main/java/com/redhat/thermostat/jvm/overview/agent/internal/model/VmInfoDAOImpl.java Wed Sep 06 19:47:49 2017 +0200 @@ -51,7 +51,7 @@ import org.apache.felix.scr.annotations.Service; import com.redhat.thermostat.agent.http.HttpRequestService; -import com.redhat.thermostat.agent.http.HttpRequestService.RequestFailedException; +import com.redhat.thermostat.agent.http.RequestFailedException; import com.redhat.thermostat.common.config.experimental.ConfigurationInfoSource; import com.redhat.thermostat.common.plugin.PluginConfiguration; import com.redhat.thermostat.common.plugin.SystemID;
--- a/plugins/vm-gc/agent/src/main/java/com/redhat/thermostat/vm/gc/agent/internal/models/VmGcStatDAOImpl.java Thu Sep 07 18:41:40 2017 +0200 +++ b/plugins/vm-gc/agent/src/main/java/com/redhat/thermostat/vm/gc/agent/internal/models/VmGcStatDAOImpl.java Wed Sep 06 19:47:49 2017 +0200 @@ -50,7 +50,7 @@ import org.apache.felix.scr.annotations.Service; import com.redhat.thermostat.agent.http.HttpRequestService; -import com.redhat.thermostat.agent.http.HttpRequestService.RequestFailedException; +import com.redhat.thermostat.agent.http.RequestFailedException; import com.redhat.thermostat.common.config.experimental.ConfigurationInfoSource; import com.redhat.thermostat.common.plugin.PluginConfiguration; import com.redhat.thermostat.common.utils.LoggingUtils;
--- a/plugins/vm-memory/agent/src/main/java/com/redhat/thermostat/vm/memory/agent/internal/models/VmMemoryStatDAOImpl.java Thu Sep 07 18:41:40 2017 +0200 +++ b/plugins/vm-memory/agent/src/main/java/com/redhat/thermostat/vm/memory/agent/internal/models/VmMemoryStatDAOImpl.java Wed Sep 06 19:47:49 2017 +0200 @@ -50,7 +50,7 @@ import org.apache.felix.scr.annotations.Service; import com.redhat.thermostat.agent.http.HttpRequestService; -import com.redhat.thermostat.agent.http.HttpRequestService.RequestFailedException; +import com.redhat.thermostat.agent.http.RequestFailedException; import com.redhat.thermostat.common.config.experimental.ConfigurationInfoSource; import com.redhat.thermostat.common.plugin.PluginConfiguration; import com.redhat.thermostat.common.utils.LoggingUtils;