diff --git a/casc/docker-compose.yml b/casc/docker-compose.yml index 1044efa7..051a06ec 100644 --- a/casc/docker-compose.yml +++ b/casc/docker-compose.yml @@ -16,6 +16,7 @@ services: container_name: jenkins networks: - crowd_net + restart: always volumes: - jenkins_home:/var/jenkins_home diff --git a/pom.xml b/pom.xml index d9458cd3..1ca144ae 100644 --- a/pom.xml +++ b/pom.xml @@ -97,8 +97,27 @@ org.apache.httpcomponents httpclient-cache + + + com.atlassian.crowd + crowd-integration-api + ${crowd-integration-client-rest.version} + + + com.atlassian.crowd + crowd-integration-client-common + ${crowd-integration-client-rest.version} + + org.jenkins-ci.plugins apache-httpcomponents-client-4-api @@ -111,6 +130,26 @@ io.jenkins.plugins jaxb + + + com.google.guava + guava + + + com.google.errorprone + error_prone_annotations + + + com.google.j2objc + j2objc-annotations + + + org.checkerframework + checker-qual + + + + junit diff --git a/src/main/java/de/theit/jenkins/crowd/CrowdConfigurationService.java b/src/main/java/de/theit/jenkins/crowd/CrowdConfigurationService.java index fbd196ee..c8a0c669 100644 --- a/src/main/java/de/theit/jenkins/crowd/CrowdConfigurationService.java +++ b/src/main/java/de/theit/jenkins/crowd/CrowdConfigurationService.java @@ -46,6 +46,8 @@ import com.atlassian.crowd.service.client.ClientProperties; import com.atlassian.crowd.service.client.ClientPropertiesImpl; import com.atlassian.crowd.service.client.CrowdClient; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import hudson.util.Secret; @@ -53,9 +55,7 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Properties; import java.util.TreeSet; import java.util.concurrent.TimeUnit; @@ -144,15 +144,15 @@ public class CrowdConfigurationService { private final Integer cacheTTL; - private CacheMap isGroupMemberCache = null; + private Cache isGroupMemberCache = null; - private CacheMap userFromSSOTokenCache = null; + private Cache userFromSSOTokenCache = null; - private CacheMap userCache = null; + private Cache userCache = null; - private CacheMap groupCache = null; + private Cache groupCache = null; - private CacheMap> authoritiesForUserCache = null; + private Cache> authoritiesForUserCache = null; /** * Creates a new Crowd configuration object. @@ -208,16 +208,16 @@ public CrowdConfigurationService(String url, String applicationName, Secret pass this.cacheTTL = cacheTTL; if (cacheSize != null && cacheSize > 0) { - this.isGroupMemberCache = new CacheMap<>(cacheSize); - this.userFromSSOTokenCache = new CacheMap<>(cacheSize); - this.userCache = new CacheMap<>(cacheSize); - this.groupCache = new CacheMap<>(cacheSize); - this.authoritiesForUserCache = new CacheMap<>(cacheSize); + this.isGroupMemberCache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheTTL, TimeUnit.MINUTES).build(); + this.userFromSSOTokenCache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheTTL, TimeUnit.MINUTES).build(); + this.userCache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheTTL, TimeUnit.MINUTES).build(); + this.groupCache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheTTL, TimeUnit.MINUTES).build(); + this.authoritiesForUserCache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheTTL, TimeUnit.MINUTES).build(); } - Properties props = getProperties(url, applicationName, Secret.toString(password), sessionValidationInterval, + Properties props = getProperties(url, applicationName, password, sessionValidationInterval, useSSO, cookieDomain, cookieTokenkey, useProxy, httpProxyHost, httpProxyPort, httpProxyUsername, - Secret.toString(httpProxyPassword), socketTimeout, httpTimeout, httpMaxConnections); + httpProxyPassword, socketTimeout, httpTimeout, httpMaxConnections); this.clientProperties = ClientPropertiesImpl.newInstanceFromProperties(props); this.crowdClient = new RestCrowdClientFactory().newInstance(this.clientProperties); this.tokenHelper = CrowdHttpTokenHelperImpl.getInstance(CrowdHttpValidationFactorExtractorImpl.getInstance()); @@ -247,8 +247,9 @@ public boolean isUseSSO() { */ public boolean isGroupMember(String username) { if (username == null) { - return false; // prevent NPE + return false; } + if (allowedGroupNames.isEmpty()) { return true; } @@ -257,7 +258,7 @@ public boolean isGroupMember(String username) { Boolean retval = getValidValueFromCache(username, isGroupMemberCache); if (retval != null) { LOG.log(Level.FINEST, "isGroupMember() cache hit: {0}", username); - return retval; + return Boolean.TRUE.equals(retval); } LOG.log(Level.FINEST, "isGroupMember() cache hit MISS: {0}", username); @@ -267,6 +268,9 @@ public boolean isGroupMember(String username) { for (String group : this.allowedGroupNames) { retval = isGroupMember(username, group); if (retval) { + // If correct object was returned save it to cache + // checking if key is present is redundant + setValueToCache(username, retval, isGroupMemberCache); break; } } @@ -281,10 +285,6 @@ public boolean isGroupMember(String username) { retval = null; } - // If correct object was returned save it to cache - // checking if key is present is redundant - setValueToCache(username, retval, isGroupMemberCache); - return Boolean.TRUE.equals(retval); } @@ -441,8 +441,6 @@ public User getUser(String username) throws UserNotFoundException, OperationFail LOG.log(Level.FINEST, "getUser() cache hit MISS: {0}", username); - LOG.log(Level.FINEST, "CrowdClient.getUser()"); - ClassLoader orgContextClassLoader = null; Thread currentThread = null; if (IS_MIN_JAVA_11) { @@ -604,6 +602,7 @@ public User findUserFromSSOToken(String token) throws OperationFailedException, ApplicationPermissionException, InvalidTokenException { // Load the entry from cache if it's valid return it User retval = getValidValueFromCache(token, userFromSSOTokenCache); + if (retval != null) { LOG.log(Level.FINEST, "findUserFromSSOToken() cache hit"); return retval; @@ -611,8 +610,6 @@ public User findUserFromSSOToken(String token) throws OperationFailedException, LOG.log(Level.FINEST, "findUserFromSSOToken() cache hit MISS"); - LOG.log(Level.FINEST, "CrowdClient.findUserFromSSOToken()"); - ClassLoader orgContextClassLoader = null; Thread currentThread = null; if (IS_MIN_JAVA_11) { @@ -773,7 +770,7 @@ private boolean isGroupMember(String username, String group) InvalidAuthenticationException, OperationFailedException { boolean retval = false; if (isGroupActive(group)) { - LOG.log(Level.FINE, "Checking group membership for user '{0}' and group '{1}'...", new Object[] {username, group}); + LOG.log(Level.FINE, "Checking group membership for user ''{0}'' and group ''{1}''...", new Object[] {username, group}); if (this.nestedGroups) { if (isUserNestedGroupMember(username, group)) { @@ -790,11 +787,11 @@ private boolean isGroupMember(String username, String group) return retval; } - private Properties getProperties(String url, String applicationName, String password, + private Properties getProperties(String url, String applicationName, Secret password, int sessionValidationInterval, boolean useSSO, String cookieDomain, String cookieTokenkey, Boolean useProxy, String httpProxyHost, String httpProxyPort, String httpProxyUsername, - String httpProxyPassword, String socketTimeout, + Secret httpProxyPassword, String socketTimeout, String httpTimeout, String httpMaxConnections) { // for // https://docs.atlassian.com/crowd/2.7.1/com/atlassian/crowd/service/client/ClientPropertiesImpl.html @@ -805,7 +802,7 @@ private Properties getProperties(String url, String applicationName, String pass crowdUrl += "/"; } props.setProperty("application.name", applicationName); - props.setProperty("application.password", password); + props.setProperty("application.password", password.getPlainText()); props.setProperty("crowd.base.url", crowdUrl); props.setProperty("application.login.url", crowdUrl + "console/"); props.setProperty("crowd.server.url", crowdUrl + "services/"); @@ -829,8 +826,8 @@ private Properties getProperties(String url, String applicationName, String pass props.setProperty("http.proxy.port", httpProxyPort); if (httpProxyUsername != null && !httpProxyUsername.equals("")) props.setProperty("http.proxy.username", httpProxyUsername); - if (httpProxyPassword != null && !httpProxyPassword.equals("")) - props.setProperty("http.proxy.password", httpProxyPassword); + if (httpProxyPassword != null && !httpProxyPassword.getPlainText().equals("")) + props.setProperty("http.proxy.password", httpProxyPassword.getPlainText()); } if (socketTimeout != null && !socketTimeout.equals("")) @@ -843,79 +840,20 @@ private Properties getProperties(String url, String applicationName, String pass return props; } - private static class CacheEntry { - private final long expires; - private final T value; - - CacheEntry(int ttlSeconds, T value) { - this.expires = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ttlSeconds); - this.value = value; - } - - public T getValue() { - return value; - } - - public boolean isValid() { - boolean isValid = System.currentTimeMillis() < expires; - LOG.log(Level.FINEST, "CacheEntry::isValid(): {0} -> {1}", new Object[] {isValid, value} ); - return isValid; - } - } - - /** - * While we could use Guava's CacheBuilder the method signature changes make - * using it problematic. Safer to roll our own and ensure compatibility across - * as wide a range of Jenkins versions as possible. - * - * @param Key type - * @param Cache entry type - */ - private static class CacheMap extends LinkedHashMap> { - - private static final long serialVersionUID = 1L; - private final int cacheSize; - - CacheMap(int cacheSize) { - super(cacheSize + 1); // prevent realloc when hitting cache size limit - this.cacheSize = cacheSize; - } - - @Override - protected boolean removeEldestEntry(Map.Entry> eldest) { - return size() > cacheSize || eldest.getValue() == null || !eldest.getValue().isValid(); - } - } - - private T getValidValueFromCache(String key, CacheMap cacheObj) { + private V getValidValueFromCache(String key, Cache cacheObj) { if (!useCache || cacheObj == null) { return null; } - final CacheEntry cached; - synchronized (this) { - cached = cacheObj.get(key); - } - - if (cached != null && cached.isValid()) { - return cached.getValue(); - } else { - return null; - } + return cacheObj.getIfPresent(key); } - private void setValueToCache(String key, T value, CacheMap cacheObj) { + private void setValueToCache(String key, V value, Cache cacheObj) { // Let's save the entry in the cache if necessary if (!useCache || value == null) { return; } - synchronized (this) { - if (cacheObj == null) { - return; - } - cacheObj.put(key, new CacheEntry<>(cacheTTL, value)); - LOG.log(Level.FINEST, "setValueToCache::cacheObj: {0}", cacheObj.toString()); - } + cacheObj.put(key, value); } } diff --git a/src/main/java/de/theit/jenkins/crowd/CrowdSecurityRealm.java b/src/main/java/de/theit/jenkins/crowd/CrowdSecurityRealm.java index 49780beb..98c0fadf 100644 --- a/src/main/java/de/theit/jenkins/crowd/CrowdSecurityRealm.java +++ b/src/main/java/de/theit/jenkins/crowd/CrowdSecurityRealm.java @@ -203,11 +203,11 @@ public CrowdSecurityRealm(String url, String applicationName, Secret password, S String cookieTokenkey, Boolean useProxy, String httpProxyHost, String httpProxyPort, String httpProxyUsername, Secret httpProxyPassword, String socketTimeout, String httpTimeout, String httpMaxConnections, CacheConfiguration cache) { - this.cookieTokenkey = cookieTokenkey; + this.cookieTokenkey = StringUtils.trimToEmpty(cookieTokenkey); this.useProxy = useProxy; - this.httpProxyHost = httpProxyHost; + this.httpProxyHost = StringUtils.trimToEmpty(httpProxyHost); this.httpProxyPort = httpProxyPort; - this.httpProxyUsername = httpProxyUsername; + this.httpProxyUsername = StringUtils.trimToEmpty(httpProxyUsername); this.httpProxyPassword = httpProxyPassword; this.socketTimeout = socketTimeout; this.httpTimeout = httpTimeout; @@ -221,6 +221,10 @@ public CrowdSecurityRealm(String url, String applicationName, Secret password, S this.useSSO = useSSO; this.cookieDomain = cookieDomain; this.cache = cache; + + // If this constructor is called, make sure to re-save the configuration. + // This way, migrated secrets are persisted securely without user interaction. + this.getDescriptor().save(); } /** @@ -265,9 +269,6 @@ public CrowdSecurityRealm(String url, String applicationName, String password, S useSSO, cookieDomain, cookieTokenkey, useProxy, httpProxyHost, httpProxyPort, httpProxyUsername, Secret.fromString(httpProxyPassword), socketTimeout, httpTimeout, httpMaxConnections, cache); - // If this constructor is called, make sure to re-save the configuration. - // This way, migrated secrets are persisted securely without user interaction. - getDescriptor().save(); } /**