mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 07:55:47 +01:00
merge with branch issue-781
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import sonia.scm.event.Event;
|
||||
|
||||
/**
|
||||
* This type of event is fired whenever a authorization relevant data changes. This event
|
||||
* is especially useful for cache invalidation.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@Event
|
||||
public final class AuthorizationChangedEvent {
|
||||
|
||||
private final String nameOfAffectedUser;
|
||||
|
||||
private AuthorizationChangedEvent(String nameOfAffectedUser) {
|
||||
this.nameOfAffectedUser = nameOfAffectedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if every user is affected by this data change.
|
||||
*
|
||||
* @return {@code true} if every user is affected
|
||||
*/
|
||||
public boolean isEveryUserAffected(){
|
||||
return nameOfAffectedUser != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the user which is affected by this event.
|
||||
*
|
||||
* @return name of affected user
|
||||
*/
|
||||
public String getNameOfAffectedUser(){
|
||||
return nameOfAffectedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new event which affects every user.
|
||||
*
|
||||
* @return new event for every user
|
||||
*/
|
||||
public static AuthorizationChangedEvent createForEveryUser() {
|
||||
return new AuthorizationChangedEvent(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event which affect a single user.
|
||||
*
|
||||
* @param nameOfAffectedUser name of affected user
|
||||
*
|
||||
* @return new event for a single user
|
||||
*/
|
||||
public static AuthorizationChangedEvent createForUser(String nameOfAffectedUser) {
|
||||
return new AuthorizationChangedEvent(nameOfAffectedUser);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -166,6 +166,7 @@ import sonia.scm.net.ahc.JsonContentTransformer;
|
||||
import sonia.scm.net.ahc.XmlContentTransformer;
|
||||
import sonia.scm.schedule.QuartzScheduler;
|
||||
import sonia.scm.schedule.Scheduler;
|
||||
import sonia.scm.security.AuthorizationChangedEventProducer;
|
||||
import sonia.scm.security.XsrfProtectionFilter;
|
||||
import sonia.scm.web.UserAgentParser;
|
||||
|
||||
@@ -300,6 +301,7 @@ public class ScmServletModule extends ServletModule
|
||||
pluginLoader.processExtensions(binder());
|
||||
|
||||
// bind security stuff
|
||||
bind(AuthorizationChangedEventProducer.class);
|
||||
bind(PermissionResolver.class, RepositoryPermissionResolver.class);
|
||||
bind(AuthenticationManager.class, ChainAuthenticatonManager.class);
|
||||
bind(SecurityContext.class).to(BasicSecurityContext.class);
|
||||
@@ -310,6 +312,7 @@ public class ScmServletModule extends ServletModule
|
||||
|
||||
// bind cache
|
||||
bind(CacheManager.class, GuavaCacheManager.class);
|
||||
bind(org.apache.shiro.cache.CacheManager.class, GuavaCacheManager.class);
|
||||
|
||||
// bind dao
|
||||
bind(GroupDAO.class, XmlGroupDAO.class);
|
||||
@@ -386,8 +389,7 @@ public class ScmServletModule extends ServletModule
|
||||
filter(PATTERN_ALL).through(BaseUrlFilter.class);
|
||||
filter(PATTERN_ALL).through(AutoLoginFilter.class);
|
||||
filterRegex(RESOURCE_REGEX).through(GZipFilter.class);
|
||||
filter(PATTERN_RESTAPI,
|
||||
PATTERN_DEBUG).through(ApiBasicAuthenticationFilter.class);
|
||||
filter(PATTERN_RESTAPI, PATTERN_DEBUG).through(ApiBasicAuthenticationFilter.class);
|
||||
filter(PATTERN_RESTAPI, PATTERN_DEBUG).through(SecurityFilter.class);
|
||||
filter(PATTERN_CONFIG, PATTERN_ADMIN).through(AdminSecurityFilter.class);
|
||||
|
||||
@@ -434,11 +436,7 @@ public class ScmServletModule extends ServletModule
|
||||
UriExtensionsConfig.class.getName());
|
||||
|
||||
String restPath = getRestPackages();
|
||||
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
logger.info("configure jersey with package path: {}", restPath);
|
||||
}
|
||||
|
||||
params.put(PackagesResourceConfig.PROPERTY_PACKAGES, restPath);
|
||||
serve(PATTERN_RESTAPI).with(GuiceContainer.class, params);
|
||||
|
||||
113
scm-webapp/src/main/java/sonia/scm/cache/GuavaBaseCache.java
vendored
Normal file
113
scm-webapp/src/main/java/sonia/scm/cache/GuavaBaseCache.java
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.cache;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Filter;
|
||||
|
||||
/**
|
||||
* Implementation of basic cache methods for guava based caches.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*
|
||||
* @since 1.52
|
||||
*
|
||||
* @param <K> key
|
||||
* @param <V> value
|
||||
*/
|
||||
public abstract class GuavaBaseCache<K, V> {
|
||||
|
||||
/**
|
||||
* the logger for GuavaBaseCache
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(GuavaCache.class);
|
||||
|
||||
protected com.google.common.cache.Cache<K, V> cache;
|
||||
protected CopyStrategy copyStrategy = CopyStrategy.NONE;
|
||||
private final String name;
|
||||
|
||||
GuavaBaseCache(com.google.common.cache.Cache<K, V> cache, CopyStrategy copyStrategy, String name) {
|
||||
this.cache = cache;
|
||||
this.name = name;
|
||||
|
||||
if (copyStrategy != null) {
|
||||
this.copyStrategy = copyStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
public void clear() {
|
||||
logger.debug("clear cache {}", name);
|
||||
cache.invalidateAll();
|
||||
}
|
||||
|
||||
public boolean contains(K key) {
|
||||
return cache.getIfPresent(key) != null;
|
||||
}
|
||||
|
||||
public boolean removeAll(Filter<K> filter) {
|
||||
Set<K> keysToRemove = Sets.newHashSet();
|
||||
|
||||
for (K key : cache.asMap().keySet()) {
|
||||
if (filter.accept(key)) {
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
boolean result = false;
|
||||
|
||||
if (!keysToRemove.isEmpty()) {
|
||||
cache.invalidateAll(keysToRemove);
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
V value = cache.getIfPresent(key);
|
||||
|
||||
if (value != null) {
|
||||
value = copyStrategy.copyOnRead(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public Cache<K, V> getWrappedCache() {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
@@ -34,18 +34,6 @@ package sonia.scm.cache;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.Filter;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -53,185 +41,22 @@ import java.util.Set;
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
*/
|
||||
public class GuavaCache<K, V> implements Cache<K, V>
|
||||
{
|
||||
public class GuavaCache<K, V> extends GuavaBaseCache<K, V> implements Cache<K, V> {
|
||||
|
||||
/**
|
||||
* the logger for GuavaCache
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GuavaCache.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param configuration
|
||||
*/
|
||||
public GuavaCache(GuavaNamedCacheConfiguration configuration)
|
||||
{
|
||||
this(configuration, configuration.getName());
|
||||
GuavaCache(com.google.common.cache.Cache<K, V> cache, CopyStrategy copyStrategy, String name) {
|
||||
super(cache, copyStrategy, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param configuration
|
||||
* @param name
|
||||
*/
|
||||
public GuavaCache(GuavaCacheConfiguration configuration, String name)
|
||||
{
|
||||
this(GuavaCaches.create(configuration, name),
|
||||
configuration.getCopyStrategy(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param cache
|
||||
* @param copyStrategy
|
||||
* @param name
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected GuavaCache(com.google.common.cache.Cache<K, V> cache,
|
||||
CopyStrategy copyStrategy, String name)
|
||||
{
|
||||
this.cache = cache;
|
||||
this.name = name;
|
||||
|
||||
if (copyStrategy != null)
|
||||
{
|
||||
this.copyStrategy = copyStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void clear()
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("clear cache {}", name);
|
||||
}
|
||||
|
||||
cache.invalidateAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param key
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean contains(K key)
|
||||
{
|
||||
return cache.getIfPresent(key) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
@Override
|
||||
public void put(K key, V value)
|
||||
{
|
||||
public void put(K key, V value) {
|
||||
cache.put(key, copyStrategy.copyOnWrite(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param key
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean remove(K key)
|
||||
{
|
||||
public boolean remove(K key) {
|
||||
cache.invalidate(key);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param filter
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean removeAll(Filter<K> filter)
|
||||
{
|
||||
Set<K> keysToRemove = Sets.newHashSet();
|
||||
|
||||
for (K key : cache.asMap().keySet())
|
||||
{
|
||||
if (filter.accept(key))
|
||||
{
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
boolean result = false;
|
||||
|
||||
if (!keysToRemove.isEmpty())
|
||||
{
|
||||
cache.invalidateAll(keysToRemove);
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param key
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public V get(K key)
|
||||
{
|
||||
V value = cache.getIfPresent(key);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
value = copyStrategy.copyOnRead(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private com.google.common.cache.Cache<K, V> cache;
|
||||
|
||||
/** Field description */
|
||||
private CopyStrategy copyStrategy = CopyStrategy.NONE;
|
||||
|
||||
/** Field description */
|
||||
private String name;
|
||||
}
|
||||
|
||||
@@ -47,66 +47,52 @@ import java.io.IOException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.cache.CacheException;
|
||||
|
||||
/**
|
||||
* Guava based implementation of {@link CacheManager} and {@link org.apache.shiro.cache.CacheManager}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
public class GuavaCacheManager implements CacheManager
|
||||
public class GuavaCacheManager implements CacheManager, org.apache.shiro.cache.CacheManager
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for GuavaCacheManager
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GuavaCacheManager.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(GuavaCacheManager.class);
|
||||
|
||||
private volatile Map<String, CacheWithConfiguration> cacheMap = Maps.newHashMap();
|
||||
|
||||
private GuavaCacheConfiguration defaultConfiguration;
|
||||
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
public GuavaCacheManager()
|
||||
{
|
||||
public GuavaCacheManager() {
|
||||
this(GuavaCacheConfigurationReader.read());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected GuavaCacheManager(GuavaCacheManagerConfiguration config)
|
||||
{
|
||||
protected GuavaCacheManager(GuavaCacheManagerConfiguration config) {
|
||||
defaultConfiguration = config.getDefaultCache();
|
||||
|
||||
for (GuavaNamedCacheConfiguration ncc : config.getCaches())
|
||||
{
|
||||
logger.debug("create cache {} from configured configuration {}",
|
||||
ncc.getName(), ncc);
|
||||
cacheMap.put(ncc.getName(), new GuavaCache(ncc));
|
||||
for (GuavaNamedCacheConfiguration ncc : config.getCaches()) {
|
||||
logger.debug("create cache {} from configured configuration {}", ncc.getName(), ncc);
|
||||
cacheMap.put(ncc.getName(), new CacheWithConfiguration(
|
||||
GuavaCaches.create(defaultConfiguration, ncc.getName()),
|
||||
defaultConfiguration)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
public void close() throws IOException {
|
||||
logger.info("close guava cache manager");
|
||||
|
||||
for (Cache c : cacheMap.values())
|
||||
{
|
||||
c.clear();
|
||||
for (CacheWithConfiguration c : cacheMap.values()) {
|
||||
c.cache.invalidateAll();
|
||||
}
|
||||
|
||||
cacheMap.clear();
|
||||
@@ -114,43 +100,44 @@ public class GuavaCacheManager implements CacheManager
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @param name
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public synchronized <K, V> GuavaCache<K, V> getCache(Class<K> key,
|
||||
Class<V> value, String name)
|
||||
{
|
||||
private synchronized <K, V> CacheWithConfiguration<K, V> getCacheWithConfiguration(String name) {
|
||||
logger.trace("try to retrieve cache {}", name);
|
||||
|
||||
GuavaCache<K, V> cache = cacheMap.get(name);
|
||||
CacheWithConfiguration<K, V> cache = cacheMap.get(name);
|
||||
|
||||
if (cache == null)
|
||||
{
|
||||
if (cache == null) {
|
||||
logger.debug(
|
||||
"cache {} does not exists, creating a new instance from default configuration: {}",
|
||||
name, defaultConfiguration);
|
||||
cache = new GuavaCache<K, V>(defaultConfiguration, name);
|
||||
name, defaultConfiguration
|
||||
);
|
||||
cache = new CacheWithConfiguration(GuavaCaches.create(defaultConfiguration, name), defaultConfiguration);
|
||||
cacheMap.put(name, cache);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
@Override
|
||||
public <K, V> GuavaCache<K, V> getCache(Class<K> key, Class<V> value, String name) {
|
||||
CacheWithConfiguration<K, V> cw = getCacheWithConfiguration(name);
|
||||
return new GuavaCache<>(cw.cache, cw.configuration.getCopyStrategy(), name);
|
||||
}
|
||||
|
||||
/** Field description */
|
||||
private volatile Map<String, GuavaCache> cacheMap = Maps.newHashMap();
|
||||
@Override
|
||||
public <K, V> GuavaSecurityCache<K, V> getCache(String name) throws CacheException {
|
||||
CacheWithConfiguration<K, V> cw = getCacheWithConfiguration(name);
|
||||
return new GuavaSecurityCache<>(cw.cache, cw.configuration.getCopyStrategy(), name);
|
||||
}
|
||||
|
||||
/** Field description */
|
||||
private GuavaCacheConfiguration defaultConfiguration;
|
||||
private static class CacheWithConfiguration<K,V> {
|
||||
|
||||
private final com.google.common.cache.Cache<K,V> cache;
|
||||
private final GuavaCacheConfiguration configuration;
|
||||
|
||||
private CacheWithConfiguration(com.google.common.cache.Cache<K, V> cache, GuavaCacheConfiguration configuration) {
|
||||
this.cache = cache;
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
83
scm-webapp/src/main/java/sonia/scm/cache/GuavaSecurityCache.java
vendored
Normal file
83
scm-webapp/src/main/java/sonia/scm/cache/GuavaSecurityCache.java
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.cache;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.apache.shiro.cache.CacheException;
|
||||
|
||||
/**
|
||||
* Guava based implementation of {@link org.apache.shiro.cache.Cache}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*
|
||||
* @since 1.52
|
||||
*
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
*/
|
||||
public class GuavaSecurityCache<K, V> extends GuavaBaseCache<K, V> implements org.apache.shiro.cache.Cache<K, V> {
|
||||
|
||||
GuavaSecurityCache(Cache<K, V> cache, CopyStrategy copyStrategy, String name) {
|
||||
super(cache, copyStrategy, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) throws CacheException {
|
||||
V previousValue = cache.getIfPresent(key);
|
||||
cache.put(key, value);
|
||||
return previousValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(K key) throws CacheException {
|
||||
V previousValue = cache.getIfPresent(key);
|
||||
cache.invalidate(key);
|
||||
return previousValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return (int) cache.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keys() {
|
||||
return cache.asMap().keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<V> values() {
|
||||
return cache.asMap().values();
|
||||
}
|
||||
|
||||
}
|
||||
105
scm-webapp/src/main/java/sonia/scm/security/AdminDetector.java
Normal file
105
scm-webapp/src/main/java/sonia/scm/security/AdminDetector.java
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
/**
|
||||
* Detects administrator from configuration.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
public class AdminDetector {
|
||||
|
||||
/**
|
||||
* the logger for AdminDetector
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AdminDetector.class);
|
||||
|
||||
private final ScmConfiguration configuration;
|
||||
|
||||
/**
|
||||
* Constructs admin detector.
|
||||
*
|
||||
* @param configuration scm-manager main configuration
|
||||
*/
|
||||
@Inject
|
||||
public AdminDetector(ScmConfiguration configuration) {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks is the authenticated user is marked as administrator by {@link ScmConfiguration}.
|
||||
*
|
||||
* @param user authenticated user
|
||||
* @param groups groups of authenticated user
|
||||
*/
|
||||
public void checkForAuthenticatedAdmin(User user, Set<String> groups) {
|
||||
if (!user.isAdmin()) {
|
||||
user.setAdmin(isAdminByConfiguration(user, groups));
|
||||
|
||||
if (LOG.isDebugEnabled() && user.isAdmin()) {
|
||||
LOG.debug("user {} is marked as admin by configuration", user.getName());
|
||||
}
|
||||
}
|
||||
else if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("authenticator {} marked user {} as admin", user.getType(), user.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAdminByConfiguration(User user, Collection<String> groups) {
|
||||
boolean result = false;
|
||||
|
||||
Set<String> adminUsers = configuration.getAdminUsers();
|
||||
if (adminUsers != null) {
|
||||
result = adminUsers.contains(user.getName());
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
Set<String> adminGroups = configuration.getAdminGroups();
|
||||
|
||||
if (adminGroups != null) {
|
||||
result = Util.containsOne(adminGroups, groups);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Set;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.DisabledAccountException;
|
||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.security.AuthenticationResult;
|
||||
|
||||
/**
|
||||
* Collects authentication info for realm.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
public class AuthenticationInfoCollector {
|
||||
|
||||
/**
|
||||
* the logger for AuthenticationInfoCollector
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationInfoCollector.class);
|
||||
|
||||
private final LocalDatabaseSynchronizer synchronizer;
|
||||
private final GroupCollector groupCollector;
|
||||
private final CredentialsStore sessionStore;
|
||||
|
||||
/**
|
||||
* Construct a new AuthenticationInfoCollector.
|
||||
*
|
||||
* @param synchronizer local database synchronizer
|
||||
* @param groupCollector groups collector
|
||||
* @param credentialsStore credentials store
|
||||
*/
|
||||
@Inject
|
||||
public AuthenticationInfoCollector(
|
||||
LocalDatabaseSynchronizer synchronizer, GroupCollector groupCollector, CredentialsStore credentialsStore
|
||||
) {
|
||||
this.synchronizer = synchronizer;
|
||||
this.groupCollector = groupCollector;
|
||||
this.sessionStore = credentialsStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates authentication info from token and authentication result.
|
||||
*
|
||||
* @param token username and password token
|
||||
* @param authenticationResult authentication result
|
||||
*
|
||||
* @return authentication info
|
||||
*/
|
||||
public AuthenticationInfo createAuthenticationInfo(
|
||||
UsernamePasswordToken token, AuthenticationResult authenticationResult
|
||||
) {
|
||||
User user = authenticationResult.getUser();
|
||||
Set<String> groups = groupCollector.collectGroups(authenticationResult);
|
||||
|
||||
synchronizer.synchronize(user, groups);
|
||||
|
||||
if (isUserDisabled(user)) {
|
||||
throwAccountIsDisabledExceptionAndLog(user.getName());
|
||||
}
|
||||
|
||||
PrincipalCollection collection = createPrincipalCollection(user, groups);
|
||||
|
||||
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(collection, token.getPassword());
|
||||
sessionStore.store(token);
|
||||
return authenticationInfo;
|
||||
}
|
||||
|
||||
private PrincipalCollection createPrincipalCollection(User user, Set<String> groups) {
|
||||
SimplePrincipalCollection collection = new SimplePrincipalCollection();
|
||||
collection.add(user.getId(), ScmRealm.NAME);
|
||||
collection.add(user, ScmRealm.NAME);
|
||||
collection.add(new GroupNames(groups), ScmRealm.NAME);
|
||||
return collection;
|
||||
}
|
||||
|
||||
private boolean isUserDisabled(User user) {
|
||||
return !user.isActive();
|
||||
}
|
||||
|
||||
private void throwAccountIsDisabledExceptionAndLog(String username) {
|
||||
String msg = "user ".concat(username).concat(" is deactivated");
|
||||
LOG.warn(msg);
|
||||
throw new DisabledAccountException(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import sonia.scm.web.security.AuthenticationManager;
|
||||
import sonia.scm.web.security.AuthenticationResult;
|
||||
|
||||
/**
|
||||
* Facade for the authentication process. The main reason for this facade is to reduce the number of constructor
|
||||
* parameters on the realm. This should improve testability.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
public class AuthenticatorFacade {
|
||||
|
||||
private final AuthenticationManager authenticator;
|
||||
private final Provider<HttpServletRequest> requestProvider;
|
||||
private final Provider<HttpServletResponse> responseProvider;
|
||||
|
||||
@Inject
|
||||
public AuthenticatorFacade(AuthenticationManager authenticator, Provider<HttpServletRequest> requestProvider,
|
||||
Provider<HttpServletResponse> responseProvider) {
|
||||
this.authenticator = authenticator;
|
||||
this.requestProvider = requestProvider;
|
||||
this.responseProvider = responseProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates the authentication request to the injected implementation of the {@link AuthenticationManager}.
|
||||
*
|
||||
* @param token username password token
|
||||
*
|
||||
* @return authentication result
|
||||
*/
|
||||
public AuthenticationResult authenticate(UsernamePasswordToken token) {
|
||||
return authenticator.authenticate(
|
||||
requestProvider.get(),
|
||||
responseProvider.get(),
|
||||
token.getUsername(),
|
||||
new String(token.getPassword())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.EagerSingleton;
|
||||
import sonia.scm.ModificationHandlerEvent;
|
||||
import sonia.scm.event.HandlerEventBase;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
|
||||
/**
|
||||
* Receives all kinds of events, which affects authorization relevant data and fires an
|
||||
* {@link AuthorizationChangedEvent} if authorization data has changed.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@EagerSingleton
|
||||
public class AuthorizationChangedEventProducer {
|
||||
|
||||
/**
|
||||
* the logger for AuthorizationChangedEventProducer
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthorizationChangedEventProducer.class);
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*/
|
||||
public AuthorizationChangedEventProducer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the
|
||||
* following reasons:
|
||||
* <ul>
|
||||
* <li>Admin or Active flag was modified.</li>
|
||||
* <li>New user created, for the case of old cache values</li>
|
||||
* <li>User deleted</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event user event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(UserEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
if (isModificationEvent(event)) {
|
||||
handleUserModificationEvent((UserModificationEvent) event);
|
||||
} else {
|
||||
handleUserEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isModificationEvent(HandlerEventBase<?> event) {
|
||||
return event instanceof ModificationHandlerEvent;
|
||||
}
|
||||
|
||||
private void handleUserEvent(UserEvent event) {
|
||||
String username = event.getItem().getName();
|
||||
logger.debug(
|
||||
"fire authorization changed event for user {}, because of user {} event", username, event.getEventType()
|
||||
);
|
||||
fireEventForUser(username);
|
||||
}
|
||||
|
||||
private void handleUserModificationEvent(UserModificationEvent event) {
|
||||
String username = event.getItem().getId();
|
||||
User beforeModification = event.getItemBeforeModification();
|
||||
if (isAuthorizationDataModified(event.getItem(), beforeModification)) {
|
||||
logger.debug(
|
||||
"fire authorization changed event for user {}, because of a authorization relevant field has changed",
|
||||
username
|
||||
);
|
||||
fireEventForUser(username);
|
||||
} else {
|
||||
logger.debug(
|
||||
"authorization changed event for user {} is not fired, because no authorization relevant field has changed",
|
||||
username
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthorizationDataModified(User user, User beforeModification) {
|
||||
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
|
||||
}
|
||||
|
||||
private void fireEventForUser(String username) {
|
||||
sendEvent(AuthorizationChangedEvent.createForUser(username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a repository has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New repository created</li>
|
||||
* <li>Repository was removed</li>
|
||||
* <li>Archived, Public readable or permission field of the repository was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event repository event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(RepositoryEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
if (isModificationEvent(event)) {
|
||||
handleRepositoryModificationEvent((RepositoryModificationEvent) event);
|
||||
} else {
|
||||
handleRepositoryEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRepositoryModificationEvent(RepositoryModificationEvent event) {
|
||||
Repository repository = event.getItem();
|
||||
if (isAuthorizationDataModified(repository, event.getItemBeforeModification())) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because a relevant field of repository {} has changed", repository.getName()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
} else {
|
||||
logger.debug(
|
||||
"authorization changed event is not fired, because non relevant field of repository {} has changed",
|
||||
repository.getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) {
|
||||
return repository.isArchived() != beforeModification.isArchived()
|
||||
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|
||||
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
|
||||
}
|
||||
|
||||
private void fireEventForEveryUser() {
|
||||
sendEvent(AuthorizationChangedEvent.createForEveryUser());
|
||||
}
|
||||
|
||||
private void handleRepositoryEvent(RepositoryEvent event){
|
||||
logger.debug(
|
||||
"fire authorization changed event, because of received {} event for repository {}",
|
||||
event.getEventType(), event.getItem().getName()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache if a group permission has changed and invalidates the cached entries of a user, if a
|
||||
* user permission has changed.
|
||||
*
|
||||
* @param event permission event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(StoredAssignedPermissionEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
StoredAssignedPermission permission = event.getPermission();
|
||||
if (permission.isGroupPermission()) {
|
||||
handleGroupPermissionChange(permission);
|
||||
} else {
|
||||
handleUserPermissionChange(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGroupPermissionChange(StoredAssignedPermission permission) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because global group permission {} has changed",
|
||||
permission.getId()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
}
|
||||
|
||||
private void handleUserPermissionChange(StoredAssignedPermission permission) {
|
||||
logger.debug(
|
||||
"fire authorization changed event for user {}, because permission {} has changed",
|
||||
permission.getName(), permission.getId()
|
||||
);
|
||||
fireEventForUser(permission.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a group has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New group created</li>
|
||||
* <li>Group was removed</li>
|
||||
* <li>Group members was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event group event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(GroupEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
if (isModificationEvent(event)) {
|
||||
handleGroupModificationEvent((GroupModificationEvent) event);
|
||||
} else {
|
||||
handleGroupEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGroupModificationEvent(GroupModificationEvent event) {
|
||||
Group group = event.getItem();
|
||||
if (isAuthorizationDataModified(group, event.getItemBeforeModification())) {
|
||||
logger.debug("fire authorization changed event, because group {} has changed", group.getId());
|
||||
fireEventForEveryUser();
|
||||
} else {
|
||||
logger.debug(
|
||||
"authorization changed event is not fired, because non relevant field of group {} has changed",
|
||||
group.getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthorizationDataModified(Group group, Group beforeModification) {
|
||||
return !group.getMembers().equals(beforeModification.getMembers());
|
||||
}
|
||||
|
||||
private void handleGroupEvent(GroupEvent event){
|
||||
logger.debug(
|
||||
"fire authorization changed event, because of received group event {} for group {}",
|
||||
event.getEventType(),
|
||||
event.getItem().getId()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void sendEvent(AuthorizationChangedEvent event) {
|
||||
ScmEventBus.getInstance().post(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,14 +57,11 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -72,10 +69,6 @@ import sonia.scm.util.Util;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import sonia.scm.Filter;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -114,8 +107,7 @@ public class AuthorizationCollector
|
||||
RepositoryDAO repositoryDAO, SecuritySystem securitySystem,
|
||||
PermissionResolver resolver)
|
||||
{
|
||||
this.cache = cacheManager.getCache(CacheKey.class, AuthorizationInfo.class,
|
||||
CACHE_NAME);
|
||||
this.cache = cacheManager.getCache(CacheKey.class, AuthorizationInfo.class, CACHE_NAME);
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
this.securitySystem = securitySystem;
|
||||
this.resolver = resolver;
|
||||
@@ -146,194 +138,14 @@ public class AuthorizationCollector
|
||||
return authorizationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the
|
||||
* following reasons:
|
||||
* <ul>
|
||||
* <li>Admin or Active flag was modified.</li>
|
||||
* <li>New user created, for the case of old cache values</li>
|
||||
* <li>User deleted</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event user event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(UserEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
User user = event.getItem();
|
||||
String username = user.getId();
|
||||
if (event instanceof UserModificationEvent)
|
||||
{
|
||||
User beforeModification = ((UserModificationEvent) event).getItemBeforeModification();
|
||||
if (shouldCacheBeCleared(user, beforeModification))
|
||||
{
|
||||
logger.debug("invalidate cache of user {}, because of a permission relevant field has changed", username);
|
||||
invalidateUserCache(username);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("cache of user {} is not invalidated, because no permission relevant field has changed", username);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("invalidate cache of user {}, because of user {} event", username, event.getEventType());
|
||||
invalidateUserCache(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldCacheBeCleared(User user, User beforeModification)
|
||||
{
|
||||
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
|
||||
}
|
||||
|
||||
private void invalidateUserCache(final String username)
|
||||
{
|
||||
cache.removeAll(new Filter<CacheKey>()
|
||||
{
|
||||
@Override
|
||||
public boolean accept(CacheKey item)
|
||||
{
|
||||
return username.equalsIgnoreCase(item.username);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a repository has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New repository created</li>
|
||||
* <li>Repository was removed</li>
|
||||
* <li>Archived, Public readable or permission field of the repository was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event repository event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(RepositoryEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
Repository repository = event.getItem();
|
||||
|
||||
if (event instanceof RepositoryModificationEvent)
|
||||
{
|
||||
Repository beforeModification = ((RepositoryModificationEvent) event).getItemBeforeModification();
|
||||
if (shouldCacheBeCleared(repository, beforeModification))
|
||||
{
|
||||
logger.debug("clear cache, because a relevant field of repository {} has changed", repository.getName());
|
||||
cache.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug(
|
||||
"cache is not invalidated, because non relevant field of repository {} has changed",
|
||||
repository.getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("clear cache, received {} event of repository {}", event.getEventType(), repository.getName());
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldCacheBeCleared(Repository repository, Repository beforeModification)
|
||||
{
|
||||
return repository.isArchived() != beforeModification.isArchived()
|
||||
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|
||||
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache if a group permission has changed and invalidates the cached entries of a user, if a
|
||||
* user permission has changed.
|
||||
*
|
||||
*
|
||||
* @param event permission event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(StoredAssignedPermissionEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
StoredAssignedPermission permission = event.getPermission();
|
||||
if (permission.isGroupPermission())
|
||||
{
|
||||
logger.debug("clear cache, because global group permission {} has changed", permission.getId());
|
||||
cache.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug(
|
||||
"clear cache of user {}, because permission {} has changed",
|
||||
permission.getName(), event.getPermission().getId()
|
||||
);
|
||||
invalidateUserCache(permission.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a group has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New group created</li>
|
||||
* <li>Group was removed</li>
|
||||
* <li>Group members was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event group event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(GroupEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
Group group = event.getItem();
|
||||
if (event instanceof GroupModificationEvent)
|
||||
{
|
||||
Group beforeModification = ((GroupModificationEvent) event).getItemBeforeModification();
|
||||
if (shouldCacheBeCleared(group, beforeModification))
|
||||
{
|
||||
logger.debug("clear cache, because group {} has changed", group.getId());
|
||||
cache.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug(
|
||||
"cache is not invalidated, because non relevant field of group {} has changed",
|
||||
group.getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("clear cache, received group event {} for group {}", event.getEventType(), group.getId());
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldCacheBeCleared(Group group, Group beforeModification)
|
||||
{
|
||||
return !group.getMembers().equals(beforeModification.getMembers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param principals
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
AuthorizationInfo collect(PrincipalCollection principals)
|
||||
public AuthorizationInfo collect(PrincipalCollection principals)
|
||||
{
|
||||
Preconditions.checkNotNull(principals, "principals parameter is required");
|
||||
|
||||
@@ -568,6 +380,30 @@ public class AuthorizationCollector
|
||||
//J+
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void invalidateCache(AuthorizationChangedEvent event) {
|
||||
if (event.isEveryUserAffected()) {
|
||||
invalidateUserCache(event.getNameOfAffectedUser());
|
||||
} else {
|
||||
invalidateCache();
|
||||
}
|
||||
}
|
||||
|
||||
private void invalidateUserCache(final String username) {
|
||||
logger.info("invalidate cache for user {}, because of a received authorization event", username);
|
||||
cache.removeAll(new Filter<CacheKey>() {
|
||||
@Override
|
||||
public boolean accept(CacheKey item) {
|
||||
return username.equalsIgnoreCase(item.username);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void invalidateCache() {
|
||||
logger.info("invalidate cache, because of a received authorization event");
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
|
||||
/**
|
||||
* Stores credentials of the user in the http session of the user.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
public class CredentialsStore {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
|
||||
|
||||
private final Provider<HttpServletRequest> requestProvider;
|
||||
|
||||
@Inject
|
||||
public CredentialsStore(Provider<HttpServletRequest> requestProvider) {
|
||||
this.requestProvider = requestProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the user credentials from token, encrypts them, and stores them in the http session.
|
||||
*
|
||||
* @param token username password token
|
||||
*/
|
||||
public void store(UsernamePasswordToken token) {
|
||||
// store encrypted credentials in session
|
||||
String credentials = token.getUsername();
|
||||
|
||||
char[] password = token.getPassword();
|
||||
if (password != null && password.length > 0) {
|
||||
credentials = credentials.concat(":").concat(new String(password));
|
||||
}
|
||||
|
||||
credentials = encrypt(credentials);
|
||||
requestProvider.get().getSession(true).setAttribute(SCM_CREDENTIALS, credentials);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected String encrypt(String credentials){
|
||||
return CipherUtil.getInstance().encode(credentials);
|
||||
}
|
||||
|
||||
}
|
||||
123
scm-webapp/src/main/java/sonia/scm/security/GroupCollector.java
Normal file
123
scm-webapp/src/main/java/sonia/scm/security/GroupCollector.java
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.util.Util;
|
||||
import sonia.scm.web.security.AuthenticationResult;
|
||||
|
||||
/**
|
||||
* Collects groups from {@link GroupManager} after authentication.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
public class GroupCollector {
|
||||
|
||||
/**
|
||||
* the logger for GroupCollector
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class);
|
||||
|
||||
private final GroupManager groupManager;
|
||||
|
||||
@Inject
|
||||
public GroupCollector(GroupManager groupManager) {
|
||||
this.groupManager = groupManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect groups from {@link AuthenticationResult} and {@link GroupManager}.
|
||||
*
|
||||
* @param authenticationResult authentication result
|
||||
*
|
||||
* @return collected groups
|
||||
*/
|
||||
public Set<String> collectGroups(AuthenticationResult authenticationResult) {
|
||||
Set<String> groups = Sets.newHashSet();
|
||||
|
||||
// add group for all authenticated users
|
||||
groups.add(GroupNames.AUTHENTICATED);
|
||||
|
||||
// load external groups
|
||||
Collection<String> groupsFromAuthenticator = authenticationResult.getGroups();
|
||||
|
||||
if (groupsFromAuthenticator != null) {
|
||||
groups.addAll(groupsFromAuthenticator);
|
||||
}
|
||||
|
||||
User user = authenticationResult.getUser();
|
||||
loadGroupFromDatabase(user, groups);
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(createMembershipLogMessage(user, groups));
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private void loadGroupFromDatabase(User user, Set<String> groupSet) {
|
||||
Collection<Group> groupCollection = groupManager.getGroupsForMember(user.getName());
|
||||
|
||||
if (groupCollection != null) {
|
||||
for (Group group : groupCollection) {
|
||||
groupSet.add(group.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String createMembershipLogMessage(User user, Set<String> groups) {
|
||||
StringBuilder msg = new StringBuilder("user ");
|
||||
|
||||
msg.append(user.getName());
|
||||
|
||||
if (Util.isNotEmpty(groups)) {
|
||||
msg.append(" is member of ");
|
||||
|
||||
Joiner.on(", ").appendTo(msg, groups);
|
||||
} else {
|
||||
msg.append(" is not a member of a group");
|
||||
}
|
||||
|
||||
return msg.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.HandlerEvent;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserDAO;
|
||||
import sonia.scm.user.UserEventHack;
|
||||
import sonia.scm.user.UserManager;
|
||||
|
||||
/**
|
||||
* Checks and synchronizes authenticated users with local database.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
public class LocalDatabaseSynchronizer {
|
||||
|
||||
/**
|
||||
* the logger for LocalDatabaseSynchronizer
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(LocalDatabaseSynchronizer.class);
|
||||
|
||||
private final AdminDetector adminSelector;
|
||||
private final UserManager userManager;
|
||||
private final UserDAO userDAO;
|
||||
|
||||
@Inject
|
||||
public LocalDatabaseSynchronizer(AdminDetector adminSelector, UserManager userManager, UserDAO userDAO) {
|
||||
this.adminSelector = adminSelector;
|
||||
this.userManager = userManager;
|
||||
this.userDAO = userDAO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for disabled and administrator marks on authenticated users and synchronize the state with the local
|
||||
* database.
|
||||
*
|
||||
* @param user authenticated user
|
||||
* @param groups groups of authenticated user
|
||||
*/
|
||||
public void synchronize(User user, Set<String> groups) {
|
||||
adminSelector.checkForAuthenticatedAdmin(user, groups);
|
||||
|
||||
User dbUser = userDAO.get(user.getId());
|
||||
if (dbUser != null) {
|
||||
synchronizeWithLocalDatabase(user, dbUser);
|
||||
} else if (user.isValid()) {
|
||||
createUserInLocalDatabase(user);
|
||||
} else {
|
||||
logger.warn("could not create user {}, beacause it is not valid", user.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void synchronizeWithLocalDatabase(User user, User dbUser) {
|
||||
synchronizeAdminFlag(user, dbUser);
|
||||
synchronizeActiveFlag(user, dbUser);
|
||||
modifyUserInLocalDatabase(user, dbUser);
|
||||
}
|
||||
|
||||
private void synchronizeAdminFlag(User user, User dbUser) {
|
||||
// if database user is an admin, set admin for the current user
|
||||
if (dbUser.isAdmin()) {
|
||||
logger.debug("user {} of type {} is marked as admin by local database", user.getName(), user.getType());
|
||||
user.setAdmin(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void synchronizeActiveFlag(User user, User dbUser) {
|
||||
// user is deactivated by database
|
||||
if (!dbUser.isActive()) {
|
||||
logger.debug("user {} is marked as deactivated by local database", user.getName());
|
||||
user.setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void createUserInLocalDatabase(User user) {
|
||||
user.setCreationDate(System.currentTimeMillis());
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.BEFORE_CREATE);
|
||||
userDAO.add(user);
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.CREATE);
|
||||
}
|
||||
|
||||
private void modifyUserInLocalDatabase(User user, User dbUser) {
|
||||
// modify existing user, copy properties except password and admin
|
||||
if (user.copyProperties(dbUser, false)) {
|
||||
user.setLastModified(System.currentTimeMillis());
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.BEFORE_MODIFY);
|
||||
userDAO.modify(user);
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.MODIFY);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,531 +35,137 @@ package sonia.scm.security;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.apache.shiro.authc.AccountException;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.DisabledAccountException;
|
||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
||||
import org.apache.shiro.authc.UnknownAccountException;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.authc.pam.UnsupportedTokenException;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.cache.CacheManager;
|
||||
import org.apache.shiro.realm.AuthorizingRealm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.HandlerEvent;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserDAO;
|
||||
import sonia.scm.user.UserEventHack;
|
||||
import sonia.scm.user.UserException;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.util.Util;
|
||||
import sonia.scm.web.security.AuthenticationManager;
|
||||
import sonia.scm.web.security.AuthenticationResult;
|
||||
import sonia.scm.web.security.AuthenticationState;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* SCM-Manager authentication realm.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
public class ScmRealm extends AuthorizingRealm
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
public static final String NAME = "scm";
|
||||
|
||||
/** Field description */
|
||||
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
|
||||
public class ScmRealm extends AuthorizingRealm {
|
||||
|
||||
/**
|
||||
* the logger for ScmRealm
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScmRealm.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
public static final String NAME = "scm";
|
||||
|
||||
|
||||
private final AuthenticatorFacade authenticator;
|
||||
private final LoginAttemptHandler loginAttemptHandler;
|
||||
private final AuthenticationInfoCollector authcCollector;
|
||||
private final AuthorizationCollector authzCollector;
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
* Constructs a new scm realm.
|
||||
*
|
||||
* @param configuration
|
||||
* @param loginAttemptHandler
|
||||
* @param collector
|
||||
* @param userManager
|
||||
* @param groupManager
|
||||
* @param userDAO
|
||||
* @param authenticator
|
||||
* @param manager
|
||||
* @param requestProvider
|
||||
* @param responseProvider
|
||||
* @param cacheManager cache manager
|
||||
* @param authenticator authenticator facade
|
||||
* @param loginAttemptHandler login attempt handler
|
||||
* @param authcCollector authentication info collector
|
||||
* @param authzCollector authorization collector
|
||||
*/
|
||||
@Inject
|
||||
public ScmRealm(ScmConfiguration configuration,
|
||||
LoginAttemptHandler loginAttemptHandler, AuthorizationCollector collector,
|
||||
UserManager userManager, GroupManager groupManager, UserDAO userDAO,
|
||||
AuthenticationManager authenticator, RepositoryManager manager,
|
||||
Provider<HttpServletRequest> requestProvider,
|
||||
Provider<HttpServletResponse> responseProvider)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.loginAttemptHandler = loginAttemptHandler;
|
||||
this.collector = collector;
|
||||
this.userManager = userManager;
|
||||
this.groupManager = groupManager;
|
||||
this.userDAO = userDAO;
|
||||
public ScmRealm(CacheManager cacheManager,
|
||||
AuthenticatorFacade authenticator, LoginAttemptHandler loginAttemptHandler,
|
||||
AuthenticationInfoCollector authcCollector, AuthorizationCollector authzCollector) {
|
||||
super(cacheManager);
|
||||
|
||||
this.authenticator = authenticator;
|
||||
this.requestProvider = requestProvider;
|
||||
this.responseProvider = responseProvider;
|
||||
this.loginAttemptHandler = loginAttemptHandler;
|
||||
this.authcCollector = authcCollector;
|
||||
this.authzCollector = authzCollector;
|
||||
|
||||
// set token class
|
||||
setAuthenticationTokenClass(UsernamePasswordToken.class);
|
||||
|
||||
// use own custom caching
|
||||
setCachingEnabled(false);
|
||||
setAuthenticationCachingEnabled(false);
|
||||
setAuthorizationCachingEnabled(false);
|
||||
|
||||
// set components
|
||||
setPermissionResolver(new RepositoryPermissionResolver());
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param authToken
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(
|
||||
AuthenticationToken authToken)
|
||||
throws AuthenticationException
|
||||
{
|
||||
if (!(authToken instanceof UsernamePasswordToken))
|
||||
{
|
||||
throw new UnsupportedTokenException("ScmAuthenticationToken is required");
|
||||
protected Object getAuthorizationCacheKey(PrincipalCollection principals) {
|
||||
return principals.getPrimaryPrincipal();
|
||||
}
|
||||
|
||||
loginAttemptHandler.beforeAuthentication(authToken);
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
|
||||
UsernamePasswordToken token = castToken(authToken);
|
||||
|
||||
UsernamePasswordToken token = (UsernamePasswordToken) authToken;
|
||||
loginAttemptHandler.beforeAuthentication(token);
|
||||
AuthenticationResult result = authenticator.authenticate(token);
|
||||
|
||||
AuthenticationInfo info = null;
|
||||
AuthenticationResult result =
|
||||
authenticator.authenticate(requestProvider.get(), responseProvider.get(),
|
||||
token.getUsername(), new String(token.getPassword()));
|
||||
|
||||
if ((result != null) && (AuthenticationState.SUCCESS == result.getState()))
|
||||
{
|
||||
loginAttemptHandler.onSuccessfulAuthentication(authToken, result);
|
||||
info = createAuthenticationInfo(token, result);
|
||||
}
|
||||
else if ((result != null)
|
||||
&& (AuthenticationState.NOT_FOUND == result.getState()))
|
||||
{
|
||||
throw new UnknownAccountException(
|
||||
"unknown account ".concat(token.getUsername()));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isSuccessful(result)) {
|
||||
loginAttemptHandler.onSuccessfulAuthentication(token, result);
|
||||
return authcCollector.createAuthenticationInfo(token, result);
|
||||
} else if (isAccountNotFound(result)) {
|
||||
throw new UnknownAccountException("unknown account ".concat(token.getUsername()));
|
||||
} else {
|
||||
loginAttemptHandler.onUnsuccessfulAuthentication(authToken, result);
|
||||
|
||||
throw new AccountException("authentication failed");
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param principals
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private UsernamePasswordToken castToken(AuthenticationToken token) {
|
||||
if (!(token instanceof UsernamePasswordToken)) {
|
||||
throw new UnsupportedTokenException("UsernamePasswordToken is required");
|
||||
}
|
||||
return (UsernamePasswordToken) token;
|
||||
}
|
||||
|
||||
private boolean isSuccessful(AuthenticationResult result) {
|
||||
return result != null && AuthenticationState.SUCCESS == result.getState();
|
||||
}
|
||||
|
||||
private boolean isAccountNotFound(AuthenticationResult result) {
|
||||
return result != null && AuthenticationState.NOT_FOUND == result.getState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(
|
||||
PrincipalCollection principals)
|
||||
{
|
||||
return collector.collect(principals);
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
|
||||
return authzCollector.collect(principals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
* @param password
|
||||
* @param ar
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Set<String> authenticate(HttpServletRequest request, String password,
|
||||
AuthenticationResult ar)
|
||||
{
|
||||
Set<String> groupSet = null;
|
||||
User user = ar.getUser();
|
||||
|
||||
try
|
||||
{
|
||||
groupSet = createGroupSet(ar);
|
||||
|
||||
// check for admin user
|
||||
checkForAuthenticatedAdmin(user, groupSet);
|
||||
|
||||
// store user
|
||||
User dbUser = userDAO.get(user.getName());
|
||||
|
||||
if (dbUser != null)
|
||||
{
|
||||
checkDBForAdmin(user, dbUser);
|
||||
checkDBForActive(user, dbUser);
|
||||
}
|
||||
|
||||
// create new user
|
||||
else if (user.isValid())
|
||||
{
|
||||
user.setCreationDate(System.currentTimeMillis());
|
||||
|
||||
// TODO find a better way
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.BEFORE_CREATE);
|
||||
userDAO.add(user);
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.CREATE);
|
||||
}
|
||||
else if (logger.isErrorEnabled())
|
||||
{
|
||||
logger.error("could not create user {}, beacause it is not valid",
|
||||
user.getName());
|
||||
}
|
||||
|
||||
if (user.isActive())
|
||||
{
|
||||
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logGroups(user, groupSet);
|
||||
}
|
||||
|
||||
// store encrypted credentials in session
|
||||
String credentials = user.getName();
|
||||
|
||||
if (Util.isNotEmpty(password))
|
||||
{
|
||||
credentials = credentials.concat(":").concat(password);
|
||||
}
|
||||
|
||||
credentials = CipherUtil.getInstance().encode(credentials);
|
||||
request.getSession(true).setAttribute(SCM_CREDENTIALS, credentials);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
String msg = "user ".concat(user.getName()).concat(" is deactivated");
|
||||
|
||||
if (logger.isWarnEnabled())
|
||||
{
|
||||
logger.warn(msg);
|
||||
}
|
||||
|
||||
throw new DisabledAccountException(msg);
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.error("authentication failed", ex);
|
||||
|
||||
throw new AuthenticationException("authentication failed", ex);
|
||||
}
|
||||
|
||||
return groupSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
* @param dbUser
|
||||
*/
|
||||
private void checkDBForActive(User user, User dbUser)
|
||||
{
|
||||
|
||||
// user is deactivated by database
|
||||
if (!dbUser.isActive())
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("user {} is marked as deactivated by local database",
|
||||
user.getName());
|
||||
}
|
||||
|
||||
user.setActive(false);
|
||||
@Subscribe
|
||||
public void invalidateCache(AuthorizationChangedEvent event) {
|
||||
if (event.isEveryUserAffected()) {
|
||||
invalidateUserCache(event.getNameOfAffectedUser());
|
||||
} else {
|
||||
invalidateCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
* @param dbUser
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws UserException
|
||||
*/
|
||||
private void checkDBForAdmin(User user, User dbUser)
|
||||
throws UserException, IOException
|
||||
{
|
||||
|
||||
// if database user is an admin, set admin for the current user
|
||||
if (dbUser.isAdmin())
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("user {} of type {} is marked as admin by local database",
|
||||
user.getName(), user.getType());
|
||||
private void invalidateUserCache(final String username) {
|
||||
logger.info("invalidate cache for user {}, because of a received authorization event", username);
|
||||
getAuthorizationCache().remove(username);
|
||||
}
|
||||
|
||||
user.setAdmin(true);
|
||||
private void invalidateCache() {
|
||||
logger.info("invalidate cache, because of a received authorization event");
|
||||
getAuthorizationCache().clear();
|
||||
}
|
||||
|
||||
// modify existing user, copy properties except password and admin
|
||||
if (user.copyProperties(dbUser, false))
|
||||
{
|
||||
user.setLastModified(System.currentTimeMillis());
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.BEFORE_MODIFY);
|
||||
userDAO.modify(user);
|
||||
UserEventHack.fireEvent(userManager, user, HandlerEvent.MODIFY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
* @param groupSet
|
||||
*/
|
||||
private void checkForAuthenticatedAdmin(User user, Set<String> groupSet)
|
||||
{
|
||||
if (!user.isAdmin())
|
||||
{
|
||||
user.setAdmin(isAdmin(user, groupSet));
|
||||
|
||||
if (logger.isDebugEnabled() && user.isAdmin())
|
||||
{
|
||||
logger.debug("user {} is marked as admin by configuration",
|
||||
user.getName());
|
||||
}
|
||||
}
|
||||
else if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("authenticator {} marked user {} as admin", user.getType(),
|
||||
user.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param token
|
||||
* @param result
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private AuthenticationInfo createAuthenticationInfo(
|
||||
UsernamePasswordToken token, AuthenticationResult result)
|
||||
{
|
||||
User user = result.getUser();
|
||||
Collection<String> groups = authenticate(requestProvider.get(),
|
||||
new String(token.getPassword()), result);
|
||||
|
||||
SimplePrincipalCollection collection = new SimplePrincipalCollection();
|
||||
|
||||
/*
|
||||
* the first (primary) principal should be a unique identifier
|
||||
*/
|
||||
collection.add(user.getId(), NAME);
|
||||
collection.add(user, NAME);
|
||||
collection.add(new GroupNames(groups), NAME);
|
||||
|
||||
return new SimpleAuthenticationInfo(collection, token.getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param ar
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Set<String> createGroupSet(AuthenticationResult ar)
|
||||
{
|
||||
Set<String> groupSet = Sets.newHashSet();
|
||||
|
||||
// add group for all authenticated users
|
||||
groupSet.add(GroupNames.AUTHENTICATED);
|
||||
|
||||
// load external groups
|
||||
Collection<String> extGroups = ar.getGroups();
|
||||
|
||||
if (extGroups != null)
|
||||
{
|
||||
groupSet.addAll(extGroups);
|
||||
}
|
||||
|
||||
// load internal groups
|
||||
loadGroups(ar.getUser(), groupSet);
|
||||
|
||||
return groupSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
* @param groupSet
|
||||
*/
|
||||
private void loadGroups(User user, Set<String> groupSet)
|
||||
{
|
||||
Collection<Group> groupCollection =
|
||||
groupManager.getGroupsForMember(user.getName());
|
||||
|
||||
if (groupCollection != null)
|
||||
{
|
||||
for (Group group : groupCollection)
|
||||
{
|
||||
groupSet.add(group.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
* @param groups
|
||||
*/
|
||||
private void logGroups(User user, Set<String> groups)
|
||||
{
|
||||
StringBuilder msg = new StringBuilder("user ");
|
||||
|
||||
msg.append(user.getName());
|
||||
|
||||
if (Util.isNotEmpty(groups))
|
||||
{
|
||||
msg.append(" is member of ");
|
||||
|
||||
Joiner.on(", ").appendTo(msg, groups);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.append(" is not a member of a group");
|
||||
}
|
||||
|
||||
logger.debug(msg.toString());
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
* @param groups
|
||||
* @return
|
||||
*/
|
||||
private boolean isAdmin(User user, Collection<String> groups)
|
||||
{
|
||||
boolean result = false;
|
||||
Set<String> adminUsers = configuration.getAdminUsers();
|
||||
|
||||
if (adminUsers != null)
|
||||
{
|
||||
result = adminUsers.contains(user.getName());
|
||||
}
|
||||
|
||||
if (!result)
|
||||
{
|
||||
Set<String> adminGroups = configuration.getAdminGroups();
|
||||
|
||||
if (adminGroups != null)
|
||||
{
|
||||
result = Util.containsOne(adminGroups, groups);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final AuthenticationManager authenticator;
|
||||
|
||||
/** Field description */
|
||||
private final AuthorizationCollector collector;
|
||||
|
||||
/** Field description */
|
||||
private final ScmConfiguration configuration;
|
||||
|
||||
/** Field description */
|
||||
private final GroupManager groupManager;
|
||||
|
||||
/** Field description */
|
||||
private final LoginAttemptHandler loginAttemptHandler;
|
||||
|
||||
/** Field description */
|
||||
private final Provider<HttpServletRequest> requestProvider;
|
||||
|
||||
/** Field description */
|
||||
private final Provider<HttpServletResponse> responseProvider;
|
||||
|
||||
/** Field description */
|
||||
private final UserDAO userDAO;
|
||||
|
||||
/** Field description */
|
||||
private final UserManager userManager;
|
||||
}
|
||||
|
||||
@@ -159,8 +159,7 @@ public abstract class CacheManagerTestBase<C extends Cache>
|
||||
* @param c1
|
||||
* @param c2
|
||||
*/
|
||||
protected void assertIsSame(Cache<String, String> c1,
|
||||
Cache<String, String> c2)
|
||||
protected void assertIsSame(Cache<String, String> c1, Cache<String, String> c2)
|
||||
{
|
||||
assertSame(c1, c2);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
package sonia.scm.cache;
|
||||
|
||||
import org.junit.Assert;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -50,4 +52,14 @@ public class GuavaCacheManagerTest extends CacheManagerTestBase
|
||||
{
|
||||
return CacheTestUtil.createDefaultGuavaCacheManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertIsSame(Cache c1, Cache c2) {
|
||||
Assert.assertSame(
|
||||
((GuavaCache)c1).getWrappedCache(),
|
||||
((GuavaCache)c2).getWrappedCache()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ public class DeactivatedUserITCase
|
||||
"slart123");
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(401, response.getStatus());
|
||||
assertEquals(403, response.getStatus());
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.inject.Provider;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||
import org.apache.shiro.realm.AuthorizingRealm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.cache.GuavaCacheManager;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.AuthorizationCollector;
|
||||
import sonia.scm.security.DefaultKeyGenerator;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.security.RepositoryPermissionResolver;
|
||||
import sonia.scm.security.SecuritySystem;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
* Performance test for {@link RepositoryManager#getAll()}.
|
||||
*
|
||||
* @see <a href="https://goo.gl/PD1AeM">Issue 781</a>
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DefaultRepositoryManagerPerfTest {
|
||||
|
||||
private static final int REPOSITORY_COUNT = 2000;
|
||||
|
||||
private static final String REPOSITORY_TYPE = "perf";
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider contextProvider;
|
||||
|
||||
@Mock
|
||||
private RepositoryDAO repositoryDAO;
|
||||
|
||||
@Mock
|
||||
private PreProcessorUtil preProcessorUtil;
|
||||
|
||||
private final ScmConfiguration configuration = new ScmConfiguration();
|
||||
|
||||
private final KeyGenerator keyGenerator = new DefaultKeyGenerator();
|
||||
|
||||
@Mock
|
||||
private RepositoryHandler repositoryHandler;
|
||||
|
||||
private DefaultRepositoryManager repositoryManager;
|
||||
|
||||
@Mock
|
||||
private SecuritySystem securitySystem;
|
||||
|
||||
private AuthorizationCollector authzCollector;
|
||||
|
||||
/**
|
||||
* Setup object under test.
|
||||
*/
|
||||
@Before
|
||||
public void setUpObjectUnderTest(){
|
||||
when(repositoryHandler.getType()).thenReturn(new Type(REPOSITORY_TYPE, REPOSITORY_TYPE));
|
||||
Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler);
|
||||
Provider<Set<RepositoryListener>> repositoryListenersProvider = new SetProvider();
|
||||
Provider<Set<RepositoryHook>> repositoryHooksProvider = new SetProvider();
|
||||
|
||||
repositoryManager = new DefaultRepositoryManager(
|
||||
configuration,
|
||||
contextProvider,
|
||||
keyGenerator,
|
||||
repositoryDAO,
|
||||
handlerSet,
|
||||
repositoryListenersProvider,
|
||||
repositoryHooksProvider,
|
||||
preProcessorUtil
|
||||
);
|
||||
|
||||
setUpTestRepositories();
|
||||
|
||||
GuavaCacheManager cacheManager = new GuavaCacheManager();
|
||||
authzCollector = new AuthorizationCollector(cacheManager, repositoryDAO, securitySystem, new RepositoryPermissionResolver());
|
||||
DefaultSecurityManager securityManager = new DefaultSecurityManager(new DummyRealm(authzCollector, cacheManager));
|
||||
|
||||
ThreadContext.bind(securityManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down test objects.
|
||||
*/
|
||||
@After
|
||||
public void tearDown(){
|
||||
ThreadContext.unbindSecurityManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start performance test and ensure that the timeout is not reached.
|
||||
*/
|
||||
@Test(timeout = 6000l)
|
||||
public void perfTestGetAll(){
|
||||
SecurityUtils.getSubject().login(new UsernamePasswordToken("trillian", "secret"));
|
||||
|
||||
List<Long> times = new ArrayList<>();
|
||||
for ( int i=0; i<3; i++ ) {
|
||||
times.add(benchGetAll());
|
||||
}
|
||||
|
||||
long average = calculateAverage(times);
|
||||
double value = (double) average / TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
|
||||
|
||||
// Too bad this functionality is not exposed as a regular method call
|
||||
System.out.println( String.format("%.4g s", value) );
|
||||
}
|
||||
|
||||
private long calculateAverage(List<Long> times) {
|
||||
Long sum = 0l;
|
||||
if(!times.isEmpty()) {
|
||||
for (Long time : times) {
|
||||
sum += time;
|
||||
}
|
||||
return Math.round(sum.doubleValue() / times.size());
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private long benchGetAll(){
|
||||
Stopwatch sw = Stopwatch.createStarted();
|
||||
System.out.append("found ").append(String.valueOf(repositoryManager.getAll().size()));
|
||||
sw.stop();
|
||||
System.out.append(" in ").println(sw);
|
||||
return sw.elapsed(TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void setUpTestRepositories() {
|
||||
Map<String,Repository> repositories = new LinkedHashMap<>();
|
||||
for ( int i=0; i<REPOSITORY_COUNT; i++ ) {
|
||||
Repository repository = createTestRepository(i);
|
||||
repositories.put(repository.getId(), repository);
|
||||
}
|
||||
when(repositoryDAO.getAll()).thenReturn(repositories.values());
|
||||
}
|
||||
|
||||
private Repository createTestRepository(int number){
|
||||
Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "repo-" + number);
|
||||
repository.getPermissions().add(new Permission("trillian", PermissionType.READ));
|
||||
return repository;
|
||||
}
|
||||
|
||||
static class DummyRealm extends AuthorizingRealm {
|
||||
|
||||
private final AuthorizationCollector authzCollector;
|
||||
|
||||
public DummyRealm(AuthorizationCollector authzCollector, org.apache.shiro.cache.CacheManager cacheManager) {
|
||||
this.authzCollector = authzCollector;
|
||||
setCredentialsMatcher(new AllowAllCredentialsMatcher());
|
||||
setCacheManager(cacheManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
|
||||
SimplePrincipalCollection spc = new SimplePrincipalCollection(token.getPrincipal(), REPOSITORY_TYPE);
|
||||
spc.add(UserTestData.createTrillian(), REPOSITORY_TYPE);
|
||||
return new SimpleAuthenticationInfo(spc, REPOSITORY_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
||||
return authzCollector.collect(principals);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SetProvider implements Provider {
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Set;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AdminDetector}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
public class AdminDetectorTest {
|
||||
|
||||
private ScmConfiguration configuration;
|
||||
private AdminDetector detector;
|
||||
|
||||
@Before
|
||||
public void setUpObjectUnderTest(){
|
||||
configuration = new ScmConfiguration();
|
||||
detector = new AdminDetector(configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AdminDetector#checkForAuthenticatedAdmin(User, Set)} with configured admin users.
|
||||
*/
|
||||
@Test
|
||||
public void testCheckForAuthenticatedAdminWithConfiguredAdminUsers() {
|
||||
configuration.setAdminUsers(ImmutableSet.of("slarti"));
|
||||
|
||||
User slarti = UserTestData.createSlarti();
|
||||
slarti.setAdmin(false);
|
||||
Set<String> groups = ImmutableSet.of();
|
||||
|
||||
detector.checkForAuthenticatedAdmin(slarti, groups);
|
||||
assertTrue(slarti.isAdmin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AdminDetector#checkForAuthenticatedAdmin(User, Set)} with configured admin group.
|
||||
*/
|
||||
@Test
|
||||
public void testCheckForAuthenticatedAdminWithConfiguredAdminGroup() {
|
||||
configuration.setAdminGroups(ImmutableSet.of("heartOfGold"));
|
||||
|
||||
User slarti = UserTestData.createSlarti();
|
||||
slarti.setAdmin(false);
|
||||
Set<String> groups = ImmutableSet.of("heartOfGold");
|
||||
|
||||
detector.checkForAuthenticatedAdmin(slarti, groups);
|
||||
assertTrue(slarti.isAdmin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AdminDetector#checkForAuthenticatedAdmin(User, Set)} with non matching configuration.
|
||||
*/
|
||||
@Test
|
||||
public void testCheckForAuthenticatedAdminWithNonMatchinConfiguration() {
|
||||
configuration.setAdminUsers(ImmutableSet.of("slarti"));
|
||||
configuration.setAdminGroups(ImmutableSet.of("heartOfGold"));
|
||||
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setAdmin(false);
|
||||
Set<String> groups = ImmutableSet.of("puzzle42");
|
||||
|
||||
detector.checkForAuthenticatedAdmin(trillian, groups);
|
||||
assertFalse(trillian.isAdmin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AdminDetector#checkForAuthenticatedAdmin(User, Set)} with user which is already admin.
|
||||
*/
|
||||
@Test
|
||||
public void testCheckForAuthenticatedAdminWithUserWhichIsAlreadyAdmin() {
|
||||
configuration.setAdminUsers(ImmutableSet.of("slarti"));
|
||||
configuration.setAdminGroups(ImmutableSet.of("heartOfGold"));
|
||||
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setAdmin(true);
|
||||
Set<String> groups = ImmutableSet.of("puzzle42");
|
||||
|
||||
detector.checkForAuthenticatedAdmin(trillian, groups);
|
||||
assertTrue(trillian.isAdmin());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Set;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.DisabledAccountException;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
import sonia.scm.web.security.AuthenticationResult;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AuthenticationInfoCollector}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class AuthenticationInfoCollectorTest {
|
||||
|
||||
@Mock
|
||||
private LocalDatabaseSynchronizer synchronizer;
|
||||
|
||||
@Mock
|
||||
private GroupCollector groupCollector;
|
||||
|
||||
@Mock
|
||||
private CredentialsStore sessionStore;
|
||||
|
||||
@InjectMocks
|
||||
private AuthenticationInfoCollector collector;
|
||||
|
||||
/**
|
||||
* Tests {@link AuthenticationInfoCollector#createAuthenticationInfo(UsernamePasswordToken, AuthenticationResult)}.
|
||||
*/
|
||||
@Test
|
||||
public void testCreateAuthenticationInfo() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
UsernamePasswordToken token = new UsernamePasswordToken(trillian.getId(), "secret");
|
||||
AuthenticationResult result = new AuthenticationResult(trillian);
|
||||
Set<String> groups = ImmutableSet.of("puzzle42", "heartOfGold");
|
||||
when(groupCollector.collectGroups(Mockito.any(AuthenticationResult.class))).thenReturn(groups);
|
||||
|
||||
AuthenticationInfo authc = collector.createAuthenticationInfo(token, result);
|
||||
verify(synchronizer).synchronize(trillian, groups);
|
||||
verify(sessionStore).store(token);
|
||||
|
||||
assertEquals(trillian.getId(), authc.getPrincipals().getPrimaryPrincipal());
|
||||
assertEquals(trillian, authc.getPrincipals().oneByType(User.class));
|
||||
assertThat(authc.getPrincipals().oneByType(GroupNames.class), contains(groups.toArray(new String[0])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthenticationInfoCollector#createAuthenticationInfo(UsernamePasswordToken, AuthenticationResult)}
|
||||
* with disabled user.
|
||||
*/
|
||||
@Test(expected = DisabledAccountException.class)
|
||||
public void testCreateAuthenticationInfoWithDisabledUser() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setActive(false);
|
||||
UsernamePasswordToken token = new UsernamePasswordToken(trillian.getId(), "secret");
|
||||
AuthenticationResult result = new AuthenticationResult(trillian);
|
||||
Set<String> groups = ImmutableSet.of("puzzle42", "heartOfGold");
|
||||
when(groupCollector.collectGroups(Mockito.any(AuthenticationResult.class))).thenReturn(groups);
|
||||
|
||||
collector.createAuthenticationInfo(token, result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
import sonia.scm.HandlerEvent;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AuthorizationChangedEventProducer}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class AuthorizationChangedEventProducerTest {
|
||||
|
||||
private StoringAuthorizationChangedEventProducer producer;
|
||||
|
||||
@Before
|
||||
public void setUpProducer() {
|
||||
producer = new StoringAuthorizationChangedEventProducer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnUserEvent()
|
||||
{
|
||||
User user = UserTestData.createDent();
|
||||
producer.onEvent(new UserEvent(user, HandlerEvent.BEFORE_CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new UserEvent(user, HandlerEvent.CREATE));
|
||||
assertUserEventIsFired("dent");
|
||||
}
|
||||
|
||||
private void assertEventIsNotFired(){
|
||||
assertNull(producer.event);
|
||||
}
|
||||
|
||||
private void assertUserEventIsFired(String username){
|
||||
assertNotNull(producer.event);
|
||||
assertTrue(producer.event.isEveryUserAffected());
|
||||
assertEquals(username, producer.event.getNameOfAffectedUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)} with modified user.
|
||||
*/
|
||||
@Test
|
||||
public void testOnUserModificationEvent()
|
||||
{
|
||||
User user = UserTestData.createDent();
|
||||
User userModified = UserTestData.createDent();
|
||||
userModified.setDisplayName("Super Dent");
|
||||
|
||||
producer.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.BEFORE_CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
userModified.setAdmin(true);
|
||||
|
||||
producer.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.BEFORE_CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.CREATE));
|
||||
assertUserEventIsFired("dent");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnGroupEvent()
|
||||
{
|
||||
Group group = new Group("xml", "base");
|
||||
producer.onEvent(new GroupEvent(group, HandlerEvent.BEFORE_CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new GroupEvent(group, HandlerEvent.CREATE));
|
||||
assertGlobalEventIsFired();
|
||||
}
|
||||
|
||||
private void assertGlobalEventIsFired(){
|
||||
assertNotNull(producer.event);
|
||||
assertFalse(producer.event.isEveryUserAffected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
|
||||
*/
|
||||
@Test
|
||||
public void testOnGroupModificationEvent()
|
||||
{
|
||||
Group group = new Group("xml", "base");
|
||||
Group modifiedGroup = new Group("xml", "base");
|
||||
producer.onEvent(new GroupModificationEvent(modifiedGroup, group, HandlerEvent.BEFORE_MODIFY));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new GroupModificationEvent(modifiedGroup, group, HandlerEvent.MODIFY));
|
||||
assertEventIsNotFired();
|
||||
|
||||
modifiedGroup.add("test");
|
||||
producer.onEvent(new GroupModificationEvent(modifiedGroup, group, HandlerEvent.MODIFY));
|
||||
assertGlobalEventIsFired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.repository.RepositoryEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnRepositoryEvent()
|
||||
{
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
producer.onEvent(new RepositoryEvent(repository, HandlerEvent.BEFORE_CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new RepositoryEvent(repository, HandlerEvent.CREATE));
|
||||
assertGlobalEventIsFired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.repository.RepositoryEvent)} with modified
|
||||
* repository.
|
||||
*/
|
||||
@Test
|
||||
public void testOnRepositoryModificationEvent()
|
||||
{
|
||||
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
|
||||
repositoryModified.setName("test123");
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
|
||||
producer.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.BEFORE_CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
producer.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
assertEventIsNotFired();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123")));
|
||||
producer.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
assertGlobalEventIsFired();
|
||||
|
||||
resetStoredEvent();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true))
|
||||
);
|
||||
producer.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
assertGlobalEventIsFired();
|
||||
|
||||
resetStoredEvent();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE))
|
||||
);
|
||||
producer.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
assertGlobalEventIsFired();
|
||||
}
|
||||
|
||||
private void resetStoredEvent(){
|
||||
producer.event = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnStoredAssignedPermissionEvent()
|
||||
{
|
||||
StoredAssignedPermission groupPermission = new StoredAssignedPermission(
|
||||
"123", new AssignedPermission("_authenticated", true, "repository:read:*")
|
||||
);
|
||||
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.BEFORE_CREATE, groupPermission));
|
||||
assertEventIsNotFired();
|
||||
|
||||
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.CREATE, groupPermission));
|
||||
assertGlobalEventIsFired();
|
||||
|
||||
resetStoredEvent();
|
||||
|
||||
StoredAssignedPermission userPermission = new StoredAssignedPermission(
|
||||
"123", new AssignedPermission("trillian", false, "repository:read:*")
|
||||
);
|
||||
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.BEFORE_CREATE, userPermission));
|
||||
assertEventIsNotFired();
|
||||
|
||||
resetStoredEvent();
|
||||
|
||||
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.CREATE, userPermission));
|
||||
assertUserEventIsFired("trillian");
|
||||
}
|
||||
|
||||
private static class StoringAuthorizationChangedEventProducer extends AuthorizationChangedEventProducer {
|
||||
|
||||
private AuthorizationChangedEvent event;
|
||||
|
||||
@Override
|
||||
protected void sendEvent(AuthorizationChangedEvent event) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,22 +54,14 @@ import static org.junit.Assert.*;
|
||||
import org.junit.Rule;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.Filter;
|
||||
import sonia.scm.HandlerEvent;
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
@@ -111,160 +103,6 @@ public class AuthorizationCollectorTest {
|
||||
collector = new AuthorizationCollector(cacheManager, repositoryDAO, securitySystem, resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnUserEvent()
|
||||
{
|
||||
User user = UserTestData.createDent();
|
||||
collector.onEvent(new UserEvent(user, HandlerEvent.BEFORE_CREATE));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new UserEvent(user, HandlerEvent.CREATE));
|
||||
verify(cache).removeAll(Mockito.any(Filter.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)} with modified user.
|
||||
*/
|
||||
@Test
|
||||
public void testOnUserModificationEvent()
|
||||
{
|
||||
User user = UserTestData.createDent();
|
||||
User userModified = UserTestData.createDent();
|
||||
userModified.setDisplayName("Super Dent");
|
||||
|
||||
collector.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.BEFORE_CREATE));
|
||||
verify(cache, never()).removeAll(Mockito.any(Filter.class));
|
||||
|
||||
collector.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.CREATE));
|
||||
verify(cache, never()).removeAll(Mockito.any(Filter.class));
|
||||
|
||||
userModified.setAdmin(true);
|
||||
|
||||
collector.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.BEFORE_CREATE));
|
||||
verify(cache, never()).removeAll(Mockito.any(Filter.class));
|
||||
|
||||
collector.onEvent(new UserModificationEvent(userModified, user, HandlerEvent.CREATE));
|
||||
verify(cache).removeAll(Mockito.any(Filter.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnGroupEvent()
|
||||
{
|
||||
Group group = new Group("xml", "base");
|
||||
collector.onEvent(new GroupEvent(group, HandlerEvent.BEFORE_CREATE));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new GroupEvent(group, HandlerEvent.CREATE));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
|
||||
*/
|
||||
@Test
|
||||
public void testOnGroupModificationEvent()
|
||||
{
|
||||
Group group = new Group("xml", "base");
|
||||
Group modifiedGroup = new Group("xml", "base");
|
||||
collector.onEvent(new GroupModificationEvent(modifiedGroup, group, HandlerEvent.BEFORE_MODIFY));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new GroupModificationEvent(modifiedGroup, group, HandlerEvent.MODIFY));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
modifiedGroup.add("test");
|
||||
collector.onEvent(new GroupModificationEvent(modifiedGroup, group, HandlerEvent.MODIFY));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnRepositoryEvent()
|
||||
{
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
collector.onEvent(new RepositoryEvent(repository, HandlerEvent.BEFORE_CREATE));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new RepositoryEvent(repository, HandlerEvent.CREATE));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)} with modified repository.
|
||||
*/
|
||||
@Test
|
||||
public void testOnRepositoryModificationEvent()
|
||||
{
|
||||
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
|
||||
repositoryModified.setName("test123");
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
|
||||
collector.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.BEFORE_CREATE));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
collector.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123")));
|
||||
collector.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
verify(cache).clear();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true))
|
||||
);
|
||||
collector.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
verify(cache, times(2)).clear();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE))
|
||||
);
|
||||
collector.onEvent(new RepositoryModificationEvent(repositoryModified, repository, HandlerEvent.CREATE));
|
||||
verify(cache, times(3)).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnStoredAssignedPermissionEvent()
|
||||
{
|
||||
StoredAssignedPermission groupPermission = new StoredAssignedPermission(
|
||||
"123", new AssignedPermission("_authenticated", true, "repository:read:*")
|
||||
);
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.BEFORE_CREATE, groupPermission));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.CREATE, groupPermission));
|
||||
verify(cache).clear();
|
||||
|
||||
|
||||
StoredAssignedPermission userPermission = new StoredAssignedPermission(
|
||||
"123", new AssignedPermission("trillian", false, "repository:read:*")
|
||||
);
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.BEFORE_CREATE, userPermission));
|
||||
verify(cache, never()).removeAll(Mockito.any(Filter.class));
|
||||
verify(cache).clear();
|
||||
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEvent.CREATE, userPermission));
|
||||
verify(cache).removeAll(Mockito.any(Filter.class));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#collect()} without user role.
|
||||
*/
|
||||
@@ -402,8 +240,7 @@ public class AuthorizationCollectorTest {
|
||||
assertThat(authInfo.getObjectPermissions(), containsInAnyOrder(wp1, wp2));
|
||||
}
|
||||
|
||||
private void authenticate(User user, String group, String... groups)
|
||||
{
|
||||
private void authenticate(User user, String group, String... groups) {
|
||||
SimplePrincipalCollection spc = new SimplePrincipalCollection();
|
||||
spc.add(user.getName(), "unit");
|
||||
spc.add(user, "unit");
|
||||
@@ -412,4 +249,16 @@ public class AuthorizationCollectorTest {
|
||||
shiro.setSubject(subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#invalidateCache(sonia.scm.security.AuthorizationChangedEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testInvalidateCache() {
|
||||
collector.invalidateCache(AuthorizationChangedEvent.createForEveryUser());
|
||||
verify(cache).clear();
|
||||
|
||||
collector.invalidateCache(AuthorizationChangedEvent.createForUser("dent"));
|
||||
verify(cache).removeAll(Mockito.any(Filter.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Provider;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.junit.Test;
|
||||
import org.junit.Before;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link CredentialsStore}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class CredentialsStoreTest {
|
||||
|
||||
@Mock
|
||||
private HttpSession session;
|
||||
|
||||
private CredentialsStore store;
|
||||
|
||||
/**
|
||||
* Set up object under test.
|
||||
*/
|
||||
@Before
|
||||
public void setUpObjectUnderTest() {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getSession(true)).thenReturn(session);
|
||||
Provider<HttpServletRequest> provider = mock(Provider.class);
|
||||
when(provider.get()).thenReturn(request);
|
||||
|
||||
store = new TestableCredentialsStore(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link CredentialsStore#store(org.apache.shiro.authc.UsernamePasswordToken)}.
|
||||
*/
|
||||
@Test
|
||||
public void testStore() {
|
||||
store.store(new UsernamePasswordToken("trillian", "trillian123"));
|
||||
verify(session).setAttribute(CredentialsStore.SCM_CREDENTIALS, "x_trillian:trillian123");
|
||||
}
|
||||
|
||||
private static class TestableCredentialsStore extends CredentialsStore {
|
||||
|
||||
public TestableCredentialsStore(Provider<HttpServletRequest> requestProvider) {
|
||||
super(requestProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String encrypt(String credentials) {
|
||||
return "x_".concat(credentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Set;
|
||||
import jdk.nashorn.internal.ir.annotations.Immutable;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.user.UserTestData;
|
||||
import sonia.scm.web.security.AuthenticationResult;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GroupCollector}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class GroupCollectorTest {
|
||||
|
||||
@Mock
|
||||
private GroupManager groupManager;
|
||||
|
||||
@InjectMocks
|
||||
private GroupCollector collector;
|
||||
|
||||
/**
|
||||
* Tests {@link GroupCollector#collectGroups(AuthenticationResult)} without groups from authenticator.
|
||||
*/
|
||||
@Test
|
||||
public void testCollectGroupsWithoutAuthenticatorGroups() {
|
||||
Set<String> groups = collector.collectGroups(new AuthenticationResult(UserTestData.createSlarti()));
|
||||
assertThat(groups, containsInAnyOrder("_authenticated"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link GroupCollector#collectGroups(AuthenticationResult)} with groups from authenticator.
|
||||
*/
|
||||
@Test
|
||||
public void testCollectGroupsWithGroupsFromAuthenticator() {
|
||||
Set<String> authGroups = ImmutableSet.of("puzzle42");
|
||||
Set<String> groups = collector.collectGroups(new AuthenticationResult(UserTestData.createSlarti(), authGroups));
|
||||
assertThat(groups, containsInAnyOrder("_authenticated", "puzzle42"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link GroupCollector#collectGroups(AuthenticationResult)} with groups from db.
|
||||
*/
|
||||
@Test
|
||||
public void testCollectGroupsWithGroupsFromDB() {
|
||||
Set<Group> dbGroups = ImmutableSet.of(new Group("test", "puzzle42"));
|
||||
when(groupManager.getGroupsForMember("slarti")).thenReturn(dbGroups);
|
||||
Set<String> groups = collector.collectGroups(new AuthenticationResult(UserTestData.createSlarti()));
|
||||
assertThat(groups, containsInAnyOrder("_authenticated", "puzzle42"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link GroupCollector#collectGroups(AuthenticationResult)} with groups from db.
|
||||
*/
|
||||
@Test
|
||||
public void testCollectGroupsWithGroupsFromDBAndAuthenticator() {
|
||||
Set<Group> dbGroups = ImmutableSet.of(new Group("test", "puzzle42"));
|
||||
Set<String> authGroups = ImmutableSet.of("heartOfGold");
|
||||
when(groupManager.getGroupsForMember("slarti")).thenReturn(dbGroups);
|
||||
Set<String> groups = collector.collectGroups(new AuthenticationResult(UserTestData.createSlarti(), authGroups));
|
||||
assertThat(groups, containsInAnyOrder("_authenticated", "puzzle42", "heartOfGold"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserDAO;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LocalDatabaseSynchronizer}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LocalDatabaseSynchronizerTest {
|
||||
|
||||
@Mock
|
||||
private AdminDetector adminSelector;
|
||||
|
||||
@Mock
|
||||
private UserManager userManager;
|
||||
|
||||
@Mock
|
||||
private UserDAO userDAO;
|
||||
|
||||
@InjectMocks
|
||||
private LocalDatabaseSynchronizer synchronizer;
|
||||
|
||||
/**
|
||||
* Tests {@link LocalDatabaseSynchronizer#synchronize(User, java.util.Set)}.
|
||||
*/
|
||||
@Test
|
||||
public void testSynchronizeWithoutDBUser() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setType("local");
|
||||
synchronizer.synchronize(trillian, ImmutableSet.<String>of());
|
||||
verify(userDAO).add(trillian);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link LocalDatabaseSynchronizer#synchronize(sonia.scm.user.User, java.util.Set)}.
|
||||
*/
|
||||
@Test
|
||||
public void testSynchronize() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setDisplayName("Trici");
|
||||
trillian.setType("local");
|
||||
trillian.setAdmin(false);
|
||||
trillian.setActive(true);
|
||||
|
||||
User dbTrillian = UserTestData.createTrillian();
|
||||
dbTrillian.setType("local");
|
||||
dbTrillian.setAdmin(true);
|
||||
dbTrillian.setActive(false);
|
||||
|
||||
when(userDAO.get(trillian.getId())).thenReturn(dbTrillian);
|
||||
|
||||
synchronizer.synchronize(trillian, ImmutableSet.<String>of());
|
||||
assertTrue(trillian.isAdmin());
|
||||
assertFalse(trillian.isActive());
|
||||
verify(userDAO).modify(trillian);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -54,8 +54,6 @@ import org.junit.Test;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.cache.MapCacheManager;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
@@ -89,6 +87,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import sonia.scm.cache.GuavaCacheManager;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -477,9 +476,23 @@ public class ScmRealmTest
|
||||
Collections.EMPTY_LIST
|
||||
);
|
||||
|
||||
CacheManager cacheManager = new MapCacheManager();
|
||||
GuavaCacheManager cacheManager = new GuavaCacheManager();
|
||||
|
||||
AuthorizationCollector collector = new AuthorizationCollector(
|
||||
AdminDetector adminSelector = new AdminDetector(new ScmConfiguration());
|
||||
LocalDatabaseSynchronizer synchronizer = new LocalDatabaseSynchronizer(
|
||||
adminSelector, userManager, userDAO
|
||||
);
|
||||
|
||||
GroupCollector groupCollector = new GroupCollector(groupManager);
|
||||
CredentialsStore sessionStore = new CredentialsStore(requestProvider);
|
||||
|
||||
AuthenticationInfoCollector authcCollector = new AuthenticationInfoCollector(
|
||||
synchronizer,
|
||||
groupCollector,
|
||||
sessionStore
|
||||
);
|
||||
|
||||
AuthorizationCollector authzCollector = new AuthorizationCollector(
|
||||
cacheManager,
|
||||
repositoryDAO,
|
||||
securitySystem,
|
||||
@@ -502,17 +515,11 @@ public class ScmRealmTest
|
||||
};
|
||||
|
||||
return new ScmRealm(
|
||||
new ScmConfiguration(),
|
||||
cacheManager,
|
||||
new AuthenticatorFacade(authManager, requestProvider, responseProvider),
|
||||
dummyLoginAttemptHandler,
|
||||
collector,
|
||||
// cacheManager,
|
||||
userManager,
|
||||
groupManager,
|
||||
userDAO,
|
||||
authManager,
|
||||
null,
|
||||
requestProvider,
|
||||
responseProvider
|
||||
authcCollector,
|
||||
authzCollector
|
||||
);
|
||||
//J+
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user