merge with branch 1.x

This commit is contained in:
Sebastian Sdorra
2017-01-12 19:50:39 +01:00
250 changed files with 16399 additions and 1573 deletions

View File

@@ -70,6 +70,10 @@ import sonia.scm.util.Util;
import java.util.List;
import java.util.Set;
import sonia.scm.group.Group;
import sonia.scm.group.GroupModificationEvent;
import sonia.scm.repository.RepositoryModificationEvent;
import sonia.scm.user.UserModificationEvent;
/**
*
@@ -80,6 +84,9 @@ import java.util.Set;
public class DefaultAuthorizationCollector implements AuthorizationCollector
{
// TODO move to util class
private static final String SEPARATOR = System.getProperty("line.separator", "\n");
/** Field description */
private static final String ADMIN_PERMISSION = "*";
@@ -139,10 +146,15 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
/**
* Method description
* 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
* @param event user event
*/
@Subscribe
public void onEvent(UserEvent event)
@@ -150,82 +162,160 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
if (event.getEventType().isPost())
{
User user = event.getItem();
if (logger.isDebugEnabled())
String username = user.getId();
if (event instanceof UserModificationEvent)
{
logger.debug(
"clear cache of user {}, because user properties have changed",
user.getName());
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);
}
// check if this is neccessary
cache.clear();
}
}
private boolean shouldCacheBeCleared(User user, User beforeModification)
{
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
}
private void invalidateUserCache(final String username)
{
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
}
/**
* Method description
* 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
* @param event repository event
*/
@Subscribe
public void onEvent(RepositoryEvent event)
{
if (event.getEventType().isPost())
{
if (logger.isDebugEnabled())
Repository repository = event.getItem();
if (event instanceof RepositoryModificationEvent)
{
logger.debug("clear cache, because repository {} has changed",
event.getItem().getName());
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();
}
cache.clear();
}
}
private boolean shouldCacheBeCleared(Repository repository, Repository beforeModification)
{
return repository.isArchived() != beforeModification.isArchived()
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
}
/**
* Method description
* 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
* @param event permission event
*/
@Subscribe
public void onEvent(StoredAssignedPermissionEvent event)
{
if (event.getEventType().isPost())
{
if (logger.isDebugEnabled())
StoredAssignedPermission permission = event.getPermission();
if (permission.isGroupPermission())
{
logger.debug("clear cache, because permission {} has changed",
event.getPermission().getId());
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());
}
cache.clear();
}
}
/**
* Method description
* 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
* @param event group event
*/
@Subscribe
public void onEvent(GroupEvent event)
{
if (event.getEventType().isPost())
{
if (logger.isDebugEnabled())
Group group = event.getItem();
if (event instanceof GroupModificationEvent)
{
logger.debug("clear cache, because group {} has changed",
event.getItem().getId());
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();
}
cache.clear();
}
}
private boolean shouldCacheBeCleared(Group group, Group beforeModification)
{
return !group.getMembers().equals(beforeModification.getMembers());
}
/**
* Method description
*
@@ -251,18 +341,13 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
if (info == null)
{
if (logger.isTraceEnabled())
{
logger.trace("collect AuthorizationInfo for user {}", user.getName());
}
logger.trace("collect AuthorizationInfo for user {}", user.getName());
info = createAuthorizationInfo(user, groupNames);
cache.put(cacheKey, info);
}
else if (logger.isTraceEnabled())
{
logger.trace("retrieve AuthorizationInfo for user {} from cache",
user.getName());
logger.trace("retrieve AuthorizationInfo for user {} from cache", user.getName());
}
return info;
@@ -271,21 +356,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private void collectGlobalPermissions(Builder<String> builder,
final User user, final GroupNames groups)
{
if (logger.isTraceEnabled())
{
logger.trace("collect global permissions for user {}", user.getName());
}
List<StoredAssignedPermission> globalPermissions =
securitySystem.getPermissions(new Predicate<AssignedPermission>()
{
@Override
public boolean apply(AssignedPermission input)
{
return isUserPermission(user, groups, input);
}
});
securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input));
for (StoredAssignedPermission gp : globalPermissions)
{
@@ -301,12 +373,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
{
for (Repository repository : repositoryDAO.getAll())
{
if (logger.isTraceEnabled())
{
logger.trace("collect permissions for repository {} and user {}",
repository.getName(), user.getName());
}
collectRepositoryPermissions(builder, repository, user, groups);
}
}
@@ -314,30 +380,36 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private void collectRepositoryPermissions(Builder<String> builder,
Repository repository, User user, GroupNames groups)
{
List<sonia.scm.repository.Permission> repositoryPermissions =
repository.getPermissions();
List<sonia.scm.repository.Permission> repositoryPermissions
= repository.getPermissions();
if (Util.isNotEmpty(repositoryPermissions))
{
boolean hasPermission = false;
for (sonia.scm.repository.Permission permission : repositoryPermissions)
{
if (isUserPermission(user, groups, permission))
if (isUserPermitted(user, groups, permission))
{
String perm = permission.getType().getPermissionPrefix().concat(
repository.getId());
logger.trace("add repository permission {} for user {}", perm,
user.getName());
String perm = permission.getType().getPermissionPrefix().concat(repository.getId());
if (logger.isTraceEnabled())
{
logger.trace("add repository permission {} for user {} at repository {}",
perm, user.getName(), repository.getName());
}
builder.add(perm);
}
}
if (!hasPermission && logger.isTraceEnabled())
{
logger.trace("no permission for user {} defined at repository {}", user.getName(), repository.getName());
}
}
else if (logger.isTraceEnabled())
{
logger.trace("repository {} has not permission entries",
logger.trace("repository {} has no permission entries",
repository.getName());
}
}
@@ -371,19 +443,47 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
info.addStringPermissions(permissions);
if (logger.isTraceEnabled()){
logger.trace(createAuthorizationSummary(user, groups, info));
}
return info;
}
private String createAuthorizationSummary(User user, GroupNames groups, AuthorizationInfo authzInfo)
{
StringBuilder buffer = new StringBuilder("authorization summary: ");
buffer.append(SEPARATOR).append("username : ").append(user.getName());
buffer.append(SEPARATOR).append("groups : ");
append(buffer, groups);
buffer.append(SEPARATOR).append("roles : ");
append(buffer, authzInfo.getRoles());
buffer.append(SEPARATOR).append("permissions:");
append(buffer, authzInfo.getStringPermissions());
append(buffer, authzInfo.getObjectPermissions());
return buffer.toString();
}
private void append(StringBuilder buffer, Iterable<?> iterable){
if (iterable != null){
for ( Object item : iterable )
{
buffer.append(SEPARATOR).append(" - ").append(item);
}
}
}
//~--- get methods ----------------------------------------------------------
private boolean isUserPermission(User user, GroupNames groups,
private boolean isUserPermitted(User user, GroupNames groups,
PermissionObject perm)
{
//J-
return (perm.isGroupPermission() && groups.contains(perm.getName()))
return (perm.isGroupPermission() && groups.contains(perm.getName()))
|| ((!perm.isGroupPermission()) && user.getName().equals(perm.getName()));
//J+
}
@@ -443,7 +543,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private final String username;
}
//~--- fields ---------------------------------------------------------------
/** authorization cache */

View File

@@ -0,0 +1,87 @@
/**
* 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 javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Util methods to handle XsrfCookies.
*
* @author Sebastian Sdorra
* @version 1.47
*/
public final class XsrfCookies
{
private XsrfCookies()
{
}
/**
* Creates a new xsrf protection cookie and add it to the response.
*
* @param request http servlet request
* @param response http servlet response
* @param token xsrf token
*/
public static void create(HttpServletRequest request, HttpServletResponse response, String token){
applyCookie(request, response, new Cookie(XsrfProtectionFilter.KEY, token));
}
/**
* Removes the current xsrf protection cookie from response.
*
* @param request http servlet request
* @param response http servlet response
*/
public static void remove(HttpServletRequest request, HttpServletResponse response)
{
Cookie[] cookies = request.getCookies();
if ( cookies != null ){
for ( Cookie c : cookies ){
if ( XsrfProtectionFilter.KEY.equals(c.getName()) ){
c.setMaxAge(0);
c.setValue(null);
applyCookie(request, response, c);
}
}
}
}
private static void applyCookie(HttpServletRequest request, HttpServletResponse response, Cookie cookie){
cookie.setPath(request.getContextPath());
response.addCookie(cookie);
}
}

View File

@@ -0,0 +1,140 @@
/**
* 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.base.Strings;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.UUID;
import javax.inject.Singleton;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Priority;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.filter.HttpFilter;
/**
* Xsrf protection http filter. The filter will issue an cookie with an xsrf protection token on the first ajax request
* of the scm web interface and marks the http session as xsrf protected. On every other request within a protected
* session, the web interface has to send the token from the cookie as http header on every request. If the filter
* receives an request to a protected session, without proper xsrf header the filter will abort the request and send an
* http error code back to the client. If the filter receives an request to a non protected session, from a non web
* interface client the filter will call the chain. The {@link XsrfProtectionFilter} is disabled by default and can be
* enabled with {@link ScmConfiguration#setEnabledXsrfProtection(boolean)}.
*
* TODO for scm-manager 2 we have to store the csrf token as part of the jwt token instead of session.
*
* @see https://bitbucket.org/sdorra/scm-manager/issues/793/json-hijacking-vulnerability-cwe-116-cwe
* @author Sebastian Sdorra
* @version 1.47
*/
@WebElement(Filters.PATTERN_RESTAPI)
@Priority(Filters.PRIORITY_PRE_BASEURL)
public final class XsrfProtectionFilter extends HttpFilter
{
/**
* the logger for XsrfProtectionFilter
*/
private static final Logger logger = LoggerFactory.getLogger(XsrfProtectionFilter.class);
/**
* Key used for session, header and cookie.
*/
static final String KEY = "X-XSRF-Token";
@Inject
public XsrfProtectionFilter(ScmConfiguration configuration)
{
this.configuration = configuration;
}
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws
IOException, ServletException
{
if (configuration.isEnabledXsrfProtection())
{
doXsrfProtection(request, response, chain);
}
else
{
logger.trace("xsrf protection is disabled, skipping check");
chain.doFilter(request, response);
}
}
private void doXsrfProtection(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws
IOException, ServletException
{
HttpSession session = request.getSession(true);
String storedToken = (String) session.getAttribute(KEY);
if ( ! Strings.isNullOrEmpty(storedToken) ){
String headerToken = request.getHeader(KEY);
if ( storedToken.equals(headerToken) ){
logger.trace("received valid xsrf protected request");
chain.doFilter(request, response);
} else {
// is forbidden the correct status code?
logger.warn("received request to a xsrf protected session without proper xsrf token");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
} else if (HttpUtil.isWUIRequest(request)) {
logger.debug("received wui request, mark session as xsrf protected and issue a new token");
String token = createToken();
session.setAttribute(KEY, token);
XsrfCookies.create(request, response, token);
chain.doFilter(request, response);
} else {
// handle non webinterface clients, which does not need xsrf protection
logger.trace("received request to a non xsrf protected session");
chain.doFilter(request, response);
}
}
private String createToken()
{
// TODO create interface and use a better method
return UUID.randomUUID().toString();
}
private ScmConfiguration configuration;
}