/**
* Copyright 2016 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.streamsets.lib.security.http;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.streamsets.datacollector.util.Configuration;
import com.streamsets.pipeline.api.impl.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class RemoteSSOService extends AbstractSSOService {
private static final Logger LOG = LoggerFactory.getLogger(RemoteSSOService.class);
public static final String DPM_BASE_URL_CONFIG = "dpm.base.url";
public static final String DPM_BASE_URL_DEFAULT = "http://localhost:18631";
public static final String SECURITY_SERVICE_APP_AUTH_TOKEN_CONFIG = CONFIG_PREFIX + "appAuthToken";
public static final String SECURITY_SERVICE_COMPONENT_ID_CONFIG = CONFIG_PREFIX + "componentId";
public static final String SECURITY_SERVICE_CONNECTION_TIMEOUT_CONFIG = CONFIG_PREFIX + "connectionTimeout.millis";
public static final int DEFAULT_SECURITY_SERVICE_CONNECTION_TIMEOUT = 10000;
public static final String DPM_ENABLED = CONFIG_PREFIX + "enabled";
public static final boolean DPM_ENABLED_DEFAULT = false;
public static final String DPM_REGISTRATION_RETRY_ATTEMPTS = "registration.retry.attempts";
public static final int DPM_REGISTRATION_RETRY_ATTEMPTS_DEFAULT = 5;
RestClient.Builder registerClientBuilder;
RestClient.Builder userAuthClientBuilder;
RestClient.Builder appAuthClientBuilder;
private String appToken;
private String componentId;
private volatile int connTimeout;
private int dpmRegistrationMaxRetryAttempts;
private volatile boolean serviceActive;
@Override
public void setConfiguration(Configuration conf) {
super.setConfiguration(conf);
String dpmBaseUrl = getValidURL(conf.get(DPM_BASE_URL_CONFIG, DPM_BASE_URL_DEFAULT));
String baseUrl = dpmBaseUrl + "security";
Utils.checkArgument(
baseUrl.toLowerCase().startsWith("http:") || baseUrl.toLowerCase().startsWith("https:"),
Utils.formatL("Security service base URL must be HTTP/HTTPS '{}'", baseUrl)
);
if (baseUrl.toLowerCase().startsWith("http://")) {
LOG.warn("Security service base URL is not secure '{}'", baseUrl);
}
setLoginPageUrl(baseUrl + "/login");
setLogoutUrl(baseUrl + "/_logout");
componentId = conf.get(SECURITY_SERVICE_COMPONENT_ID_CONFIG, null);
appToken = conf.get(SECURITY_SERVICE_APP_AUTH_TOKEN_CONFIG, null);
connTimeout = conf.get(SECURITY_SERVICE_CONNECTION_TIMEOUT_CONFIG, DEFAULT_SECURITY_SERVICE_CONNECTION_TIMEOUT);
dpmRegistrationMaxRetryAttempts = conf.get(DPM_REGISTRATION_RETRY_ATTEMPTS,
DPM_REGISTRATION_RETRY_ATTEMPTS_DEFAULT);
registerClientBuilder = RestClient.builder(baseUrl)
.csrf(true)
.json(true)
.path("public-rest/v1/components/registration")
.timeout(connTimeout);
userAuthClientBuilder = RestClient.builder(baseUrl)
.csrf(true)
.json(true)
.path("rest/v1/validateAuthToken/user")
.timeout(connTimeout);
appAuthClientBuilder = RestClient.builder(baseUrl)
.csrf(true)
.json(true)
.path("rest/v1/validateAuthToken/component")
.timeout(connTimeout);
}
@VisibleForTesting
public RestClient.Builder getRegisterClientBuilder() {
return registerClientBuilder;
}
@VisibleForTesting
public RestClient.Builder getUserAuthClientBuilder() {
return userAuthClientBuilder;
}
@VisibleForTesting
public RestClient.Builder getAppAuthClientBuilder() {
return appAuthClientBuilder;
}
@VisibleForTesting
void sleep(int secs) {
try {
Thread.sleep(secs * 1000);
} catch (InterruptedException ex) {
String msg = "Interrupted while attempting DPM registration";
LOG.error(msg);
throw new RuntimeException(msg, ex);
}
}
void updateConnectionTimeout(RestClient.Response response) {
String timeout = response.getHeader(SSOConstants.X_APP_CONNECTION_TIMEOUT);
connTimeout = (timeout == null) ? connTimeout: Integer.parseInt(timeout);
}
boolean checkServiceActive() {
boolean active;
try {
URL url = new URL(getLoginPageUrl());
int status = ((HttpURLConnection)url.openConnection()).getResponseCode();
active = status == HttpURLConnection.HTTP_OK;
if (!active) {
LOG.warn("DPM reachable but returning '{}' HTTP status on login", status);
}
} catch (IOException ex) {
LOG.warn("DPM not reachable: {}", ex.toString());
active = false;
}
LOG.debug("DPM current status '{}'", (active) ? "ACTIVE" : "NON ACTIVE");
return active;
}
public boolean isServiceActive(boolean checkNow) {
if (checkNow) {
serviceActive = checkServiceActive();
}
return serviceActive;
}
@Override
public void register(Map<String, String> attributes) {
if (appToken.isEmpty() || componentId.isEmpty()) {
if (appToken.isEmpty()) {
LOG.warn("Skipping component registration to DPM, application auth token is not set");
}
if (componentId.isEmpty()) {
LOG.warn("Skipping component registration to DPM, component ID is not set");
}
throw new RuntimeException("Registration to DPM not done, missing component ID or app auth token");
} else {
LOG.debug("Doing component ID '{}' registration with DPM", componentId);
Map<String, Object> registrationData = new HashMap<>();
registrationData.put("authToken", appToken);
registrationData.put("componentId", componentId);
registrationData.put("attributes", attributes);
int delaySecs = 1;
int attempts = 0;
boolean registered = false;
//When Load Balancer(HAProxy or ELB) is used, it will take couple of seconds for load balancer to access
//security service. So we are retrying registration couple of times until server is accessible via load balancer.
while (attempts < dpmRegistrationMaxRetryAttempts) {
if (attempts > 0) {
delaySecs = delaySecs * 2;
delaySecs = Math.min(delaySecs, 16);
LOG.warn("DPM registration attempt '{}', waiting for '{}' seconds before retrying ...", attempts, delaySecs);
sleep(delaySecs);
}
attempts++;
try {
RestClient restClient = getRegisterClientBuilder().build();
RestClient.Response response = restClient.post(registrationData);
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
updateConnectionTimeout(response);
LOG.info("Registered with DPM");
registered = true;
break;
} else if (response.getStatus() == HttpURLConnection.HTTP_UNAVAILABLE) {
LOG.warn("DPM Registration unavailable");
} else if (response.getStatus() == HttpURLConnection.HTTP_FORBIDDEN) {
throw new RuntimeException(Utils.format(
"Failed registration for component ID '{}': {}",
componentId,
response.getError()
));
} else {
LOG.warn("Failed to registered to DPM, HTTP status '{}': {}", response.getStatus(), response.getError());
break;
}
} catch (IOException ex) {
LOG.warn("DPM Registration failed: {}", ex.toString());
}
}
if (registered) {
clearCaches();
serviceActive = true;
} else {
LOG.warn("DPM registration failed after '{}' attempts", attempts);
}
}
}
public void setComponentId(String componentId) {
componentId = (componentId != null) ? componentId.trim() : null;
Utils.checkArgument(componentId != null && !componentId.isEmpty(), "Component ID cannot be NULL or empty");
this.componentId = componentId;
registerClientBuilder.componentId(componentId);
userAuthClientBuilder.componentId(componentId);
appAuthClientBuilder.componentId(componentId);
}
public void setApplicationAuthToken(String appToken) {
appToken = (appToken != null) ? appToken.trim() : null;
this.appToken = appToken;
registerClientBuilder.appAuthToken(appToken);
userAuthClientBuilder.appAuthToken(appToken);
appAuthClientBuilder.appAuthToken(appToken);
}
private boolean checkServiceActiveIfInActive() {
if (!serviceActive) {
serviceActive = checkServiceActive();
}
return serviceActive;
}
protected SSOPrincipal validateUserTokenWithSecurityService(String userAuthToken)
throws ForbiddenException {
Utils.checkState(checkServiceActiveIfInActive(), "Security service not active");
ValidateUserAuthTokenJson authTokenJson = new ValidateUserAuthTokenJson();
authTokenJson.setAuthToken(userAuthToken);
SSOPrincipalJson principal;
try {
RestClient restClient = getUserAuthClientBuilder().build();
RestClient.Response response = restClient.post(authTokenJson);
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
updateConnectionTimeout(response);
principal = response.getData(SSOPrincipalJson.class);
} else if (response.getStatus() == HttpURLConnection.HTTP_FORBIDDEN) {
throw new ForbiddenException(response.getError());
} else {
throw new RuntimeException(Utils.format(
"Could not validate user token '{}', HTTP status '{}' message: {}",
null,
response.getStatus(),
response.getError()
));
}
} catch (IOException ex){
LOG.warn("Could not do user token validation, going inactive: {}", ex.toString());
serviceActive = false;
Map error = ImmutableMap.of("message", "Could not connect to security service: " + ex.toString());
throw new ForbiddenException(error);
}
if (principal != null) {
principal.setTokenStr(userAuthToken);
principal.lock();
LOG.debug("Validated user auth token for '{}'", principal.getPrincipalId());
}
return principal;
}
protected SSOPrincipal validateAppTokenWithSecurityService(String authToken, String componentId)
throws ForbiddenException {
Utils.checkState(checkServiceActiveIfInActive(), "Security service not active");
ValidateComponentAuthTokenJson authTokenJson = new ValidateComponentAuthTokenJson();
authTokenJson.setComponentId(componentId);
authTokenJson.setAuthToken(authToken);
SSOPrincipalJson principal;
try {
RestClient restClient = getAppAuthClientBuilder().build();
RestClient.Response response = restClient.post(authTokenJson);
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
updateConnectionTimeout(response);
principal = response.getData(SSOPrincipalJson.class);
} else if (response.getStatus() == HttpURLConnection.HTTP_FORBIDDEN) {
throw new ForbiddenException(response.getError());
} else {
throw new RuntimeException(Utils.format(
"Could not validate app token for component ID '{}', HTTP status '{}' message: {}",
componentId,
response.getStatus(),
response.getError()
));
}
} catch (IOException ex){
LOG.warn("Could not do app token validation, going inactive: {}", ex.toString());
serviceActive = false;
Map error = ImmutableMap.of("message", "Could not connect to seucirty service: " + ex.toString());
throw new ForbiddenException(error);
}
if (principal != null) {
principal.setTokenStr(authToken);
principal.lock();
LOG.debug("Validated app auth token for '{}'", principal.getPrincipalId());
}
return principal;
}
public static String getValidURL(String url) {
if (!url.endsWith("/")) {
url += "/";
}
return url;
}
@VisibleForTesting
int getConnectionTimeout() {
return connTimeout;
}
}