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();
}
/**