/** * Copyright 2016 StreamSets Inc. * <p> * 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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.streamsets.datacollector.util.Configuration; import com.streamsets.pipeline.api.impl.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; public abstract class AbstractSSOService implements SSOService { private static final Logger LOG = LoggerFactory.getLogger(AbstractSSOService.class); public static final String CONFIG_PREFIX = "dpm."; public static final String SECURITY_SERVICE_VALIDATE_AUTH_TOKEN_FREQ_CONFIG = CONFIG_PREFIX + "security.validationTokenFrequency.secs"; public static final long SECURITY_SERVICE_VALIDATE_AUTH_TOKEN_FREQ_DEFAULT = 60; private String loginPageUrl; private String logoutUrl; private PrincipalCache userPrincipalCache; private PrincipalCache appPrincipalCache; @Override public void setDelegateTo(SSOService ssoService) { throw new UnsupportedOperationException(); } @Override public SSOService getDelegateTo() { throw new UnsupportedOperationException(); } @Override public void setConfiguration(Configuration conf) { long validateAuthTokenFrequencySecs = conf.get(SECURITY_SERVICE_VALIDATE_AUTH_TOKEN_FREQ_CONFIG, SECURITY_SERVICE_VALIDATE_AUTH_TOKEN_FREQ_DEFAULT); Utils.checkArgument(validateAuthTokenFrequencySecs >= 30, Utils.format( "Configuration '{}' set to '{}' seconds, it must be at least '{}' secs", SECURITY_SERVICE_VALIDATE_AUTH_TOKEN_FREQ_CONFIG, validateAuthTokenFrequencySecs, 30 )); initializePrincipalCaches(TimeUnit.SECONDS.toMillis(validateAuthTokenFrequencySecs)); } protected void setLoginPageUrl(String loginPageUrl) { this.loginPageUrl = loginPageUrl; } protected void setLogoutUrl(String logoutUrl) { this.logoutUrl = logoutUrl; } void initializePrincipalCaches(long ttlMillis) { //for user tokens, once a token is invalid that it, so we should cache the invalid one for a while to avoid the // full check userPrincipalCache = new PrincipalCache(ttlMillis, TimeUnit.HOURS.toMillis(1)); // for app tokens, an app token can be invalid because of being deactivated but once reactivated it will be // valid again, so the caching time should be the same as for valid app tokens appPrincipalCache = new PrincipalCache(ttlMillis, ttlMillis); } protected PrincipalCache getUserPrincipalCache() { return userPrincipalCache; } protected PrincipalCache getAppPrincipalCache() { return appPrincipalCache; } @Override public String createRedirectToLoginUrl(String requestUrl, boolean repeatedRedirect) { try { String url = loginPageUrl + "?" + SSOConstants.REQUESTED_URL_PARAM + "=" + URLEncoder.encode(requestUrl, "UTF-8"); if (repeatedRedirect) { url = url + "&" + SSOConstants.REPEATED_REDIRECT_PARAM + "="; } return url; } catch (UnsupportedEncodingException ex) { throw new RuntimeException(Utils.format("Should not happen: {}", ex.toString()), ex); } } String getLoginPageUrl() { return loginPageUrl; } @Override public String getLogoutUrl() { return logoutUrl; } @Override public SSOPrincipal validateUserToken(String authToken) { return validate(userPrincipalCache, createUserRemoteValidation(authToken), authToken, "-", "User"); } @Override public boolean invalidateUserToken(String authToken) { return userPrincipalCache.invalidate(authToken); } // returns principal if OK, throws ForbiddenException if invalid credentials (will add to invalid cache) or // throws RuntimeException (if connection issues, won't add to invalid cache protected abstract SSOPrincipal validateUserTokenWithSecurityService(String authToken) throws ForbiddenException; @Override public SSOPrincipal validateAppToken(String authToken, String componentId) { SSOPrincipal principal = validate(appPrincipalCache, createAppRemoteValidation(authToken, componentId), authToken, componentId, "App"); if (principal != null && !principal.getPrincipalId().equals(componentId)) { principal = null; } return principal; } @Override public boolean invalidateAppToken(String authToken) { return appPrincipalCache.invalidate(authToken); } @Override public void clearCaches() { getUserPrincipalCache().clear(); getAppPrincipalCache().clear(); LOG.info("Flushed user and application principal caches"); } // returns principal if OK, throws ForbiddenException if invalid credentials (will add to invalid cache) or // throws RuntimeException (if connection issues, won't add to invalid cache protected abstract SSOPrincipal validateAppTokenWithSecurityService(String authToken, String componentId) throws ForbiddenException; private static final Object DUMMY = new Object(); private ConcurrentMap<String, Object> lockMap = new ConcurrentHashMap<>(); @VisibleForTesting ConcurrentMap<String, Object> getLockMap() { return lockMap; } private void trace(String message, String token, String component) { if (LOG.isTraceEnabled()) { LOG.trace(message, SSOUtils.tokenForLog(token), component); } } SSOPrincipal validate(PrincipalCache cache, Callable<SSOPrincipal> remoteValidation, String token, String componentId, String type) { SSOPrincipal principal = cache.get(token); String tokenForLog = SSOUtils.tokenForLog(token); if (principal == null) { if (cache.isInvalid(token)) { LOG.debug("Token '{}' invalid '{}' for component '{}'", type, tokenForLog, componentId); } else { trace("Trying to get lock for token '{}' component '{}'", tokenForLog, componentId); long start = System.currentTimeMillis(); int counter = 0; while (getLockMap().putIfAbsent(token, DUMMY) != null) { if (++counter % 1000 == 0) { trace("Retrying getting lock for token '{}' component '{}'", tokenForLog, componentId); } counter++; if (System.currentTimeMillis() - start > 10000) { String msg = Utils.format("Exceeded 10sec max wait time trying to validate component '{}'", componentId); LOG.warn(msg); throw new RuntimeException(msg); } try { Thread.sleep(10); } catch (InterruptedException ex) { LOG.warn( "Got interrupted while waiting for lock for token '{}' for component '{}'", tokenForLog, componentId ); return null; } } trace("Got lock for token '{}' component '{}'", token, componentId); try { principal = cache.get(token); if (principal == null) { LOG.debug("Token '{}' component '{}' not found in cache", tokenForLog, componentId); try { principal = remoteValidation.call(); trace("Adding token '{}' for component '{}' to cache", tokenForLog, componentId); cache.put(token, principal); } catch (ForbiddenException fex) { cache.invalidate(token); trace("ForbiddenToken '{}' invalid '{}', invalidating in cache", tokenForLog, componentId); throw fex; } catch (Exception ex) { if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else { throw new RuntimeException(ex); } } } else { LOG.debug("Token '{}' component '{}' found in cache", tokenForLog, componentId); } } catch (Exception ex) { LOG.error( "Exception while doing remote validation for token '{}' component '{}': {}", tokenForLog, componentId, ex.toString() ); } finally { trace("Released lock for token '{}' component '{}'", tokenForLog, componentId); getLockMap().remove(token); } } } return principal; } Callable<SSOPrincipal> createUserRemoteValidation(final String authToken) { return new Callable<SSOPrincipal>() { @Override public SSOPrincipal call() throws Exception { return validateUserTokenWithSecurityService(authToken); } }; } Callable<SSOPrincipal> createAppRemoteValidation(final String authToken, final String componentId) { return new Callable<SSOPrincipal>() { @Override public SSOPrincipal call() throws Exception { return validateAppTokenWithSecurityService(authToken, componentId); } }; } }