merge 2.0.0-m3

This commit is contained in:
Eduard Heimbuch
2019-08-15 10:51:36 +02:00
172 changed files with 3320 additions and 1792 deletions

View File

@@ -33,6 +33,11 @@ class EdisonHalAppender implements HalAppender {
embeddedBuilder.with(rel, embedded);
}
@Override
public void appendEmbedded(String rel, List<HalRepresentation> embedded) {
embeddedBuilder.with(rel, embedded);
}
private static class EdisonLinkArrayBuilder implements LinkArrayBuilder {
private final Links.Builder builder;

View File

@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.security.PermissionPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -47,6 +48,7 @@ public class GroupPermissionResource {
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getPermissions(@PathParam("id") String id) {
PermissionPermissions.read().check();
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForGroup(id);
return Response.ok(permissionCollectionToDtoMapper.mapForGroup(permissions, id)).build();
}

View File

@@ -7,7 +7,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
import java.util.Set;
@Getter
@Setter
@@ -17,7 +17,7 @@ public class MeDto extends HalRepresentation {
private String name;
private String displayName;
private String mail;
private List<String> groups;
private Set<String> groups;
MeDto(Links links, Embedded embedded) {
super(links, embedded);

View File

@@ -1,18 +1,16 @@
package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableList;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupCollector;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import java.util.Collections;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
@@ -22,11 +20,13 @@ public class MeDtoFactory extends HalAppenderMapper {
private final ResourceLinks resourceLinks;
private final UserManager userManager;
private final GroupCollector groupCollector;
@Inject
public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) {
public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector) {
this.resourceLinks = resourceLinks;
this.userManager = userManager;
this.groupCollector = groupCollector;
}
public MeDto create() {
@@ -35,16 +35,12 @@ public class MeDtoFactory extends HalAppenderMapper {
MeDto dto = createDto(user);
mapUserProperties(user, dto);
mapGroups(principals, dto);
mapGroups(user, dto);
return dto;
}
private void mapGroups(PrincipalCollection principals, MeDto dto) {
Iterable<String> groups = principals.oneByType(GroupNames.class);
if (groups == null) {
groups = Collections.emptySet();
}
dto.setGroups(ImmutableList.copyOf(groups));
private void mapGroups(User user, MeDto dto) {
dto.setGroups(groupCollector.collect(user.getName()));
}
private void mapUserProperties(User user, MeDto dto) {

View File

@@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.MergeCommandBuilder;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
@@ -49,6 +50,7 @@ public class MergeResource {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
RepositoryPermissions.push(repositoryService.getRepository()).check();
MergeCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).executeMerge();
if (mergeCommandResult.isSuccess()) {
return Response.noContent().build();
@@ -67,14 +69,19 @@ public class MergeResource {
@ResponseCode(code = 500, condition = "internal server error")
})
public Response dryRun(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun();
if (mergeCommandResult.isMergeable()) {
return Response.noContent().build();
if (RepositoryPermissions.push(repositoryService.getRepository()).isPermitted()) {
MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun();
if (mergeCommandResult.isMergeable()) {
return Response.noContent().build();
} else {
throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision());
}
} else {
throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision());
return Response.noContent().build();
}
}
}

View File

@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.security.PermissionPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -48,6 +49,7 @@ public class UserPermissionResource {
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getPermissions(@PathParam("id") String id) {
PermissionPermissions.read().check();
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForUser(id);
return Response.ok(permissionCollectionToDtoMapper.mapForUser(permissions, id)).build();
}

View File

@@ -0,0 +1,72 @@
package sonia.scm.group;
import com.cronutils.utils.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Set;
/**
* Collect groups for a certain principal.
* <strong>Warning</strong>: The class is only for internal use and should never used directly.
*/
@Singleton
public class DefaultGroupCollector implements GroupCollector {
private static final Logger LOG = LoggerFactory.getLogger(DefaultGroupCollector.class);
@VisibleForTesting
static final String CACHE_NAME = "sonia.cache.externalGroups";
private final GroupDAO groupDAO;
private final Cache<String, Set<String>> cache;
private final Set<GroupResolver> groupResolvers;
@Inject
public DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set<GroupResolver> groupResolvers) {
this.groupDAO = groupDAO;
this.cache = cacheManager.getCache(CACHE_NAME);
this.groupResolvers = groupResolvers;
}
@Override
public Set<String> collect(String principal) {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
builder.add(AUTHENTICATED);
builder.addAll(resolveExternalGroups(principal));
appendInternalGroups(principal, builder);
Set<String> groups = builder.build();
LOG.debug("collected following groups for principal {}: {}", principal, groups);
return groups;
}
private void appendInternalGroups(String principal, ImmutableSet.Builder<String> builder) {
for (Group group : groupDAO.getAll()) {
if (group.isMember(principal)) {
builder.add(group.getName());
}
}
}
private Set<String> resolveExternalGroups(String principal) {
Set<String> externalGroups = cache.get(principal);
if (externalGroups == null) {
ImmutableSet.Builder<String> newExternalGroups = ImmutableSet.builder();
for (GroupResolver groupResolver : groupResolvers) {
newExternalGroups.addAll(groupResolver.resolve(principal));
}
externalGroups = newExternalGroups.build();
cache.put(principal, externalGroups);
}
return externalGroups;
}
}

View File

@@ -1,6 +1,7 @@
package sonia.scm.lifecycle.modules;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.google.inject.throwingproviders.ThrowingProviderBinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -10,6 +11,7 @@ import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.xml.MetadataStore;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.security.CipherHandler;
import sonia.scm.security.CipherUtil;
@@ -19,15 +21,20 @@ import sonia.scm.store.BlobStoreFactory;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.store.DefaultBlobDirectoryAccess;
import sonia.scm.store.FileBlobStoreFactory;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.store.JAXBDataStoreFactory;
import sonia.scm.store.JAXBPropertyFileAccess;
import sonia.scm.update.BlobDirectoryAccess;
import sonia.scm.update.PropertyFileAccess;
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
import sonia.scm.update.V1PropertyDAO;
import sonia.scm.update.xml.XmlV1PropertyDAO;
import java.nio.file.Path;
public class BootstrapModule extends AbstractModule {
private static final Logger LOG = LoggerFactory.getLogger(BootstrapModule.class);
@@ -65,6 +72,8 @@ public class BootstrapModule extends AbstractModule {
bind(PluginLoader.class).toInstance(pluginLoader);
bind(V1PropertyDAO.class, XmlV1PropertyDAO.class);
bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class);
bind(BlobDirectoryAccess.class, DefaultBlobDirectoryAccess.class);
bind(new TypeLiteral<UpdateStepRepositoryMetadataAccess<Path>>() {}).to(new TypeLiteral<MetadataStore>() {});
}
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {

View File

@@ -50,13 +50,16 @@ import sonia.scm.cache.CacheManager;
import sonia.scm.cache.GuavaCacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus;
import sonia.scm.group.DefaultGroupCollector;
import sonia.scm.group.DefaultGroupDisplayManager;
import sonia.scm.group.DefaultGroupManager;
import sonia.scm.group.GroupCollector;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupDisplayManager;
import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupManagerProvider;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.migration.MigrationDAO;
import sonia.scm.net.SSLContextProvider;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.ContentTransformer;
@@ -97,6 +100,7 @@ import sonia.scm.template.MustacheTemplateEngine;
import sonia.scm.template.TemplateEngine;
import sonia.scm.template.TemplateEngineFactory;
import sonia.scm.template.TemplateServlet;
import sonia.scm.update.repository.DefaultMigrationStrategyDAO;
import sonia.scm.user.DefaultUserDisplayManager;
import sonia.scm.user.DefaultUserManager;
import sonia.scm.user.UserDAO;
@@ -183,6 +187,7 @@ class ScmServletModule extends ServletModule {
bind(RepositoryDAO.class, XmlRepositoryDAO.class);
bind(RepositoryRoleDAO.class, XmlRepositoryRoleDAO.class);
bind(RepositoryRoleManager.class).to(DefaultRepositoryRoleManager.class);
bind(MigrationDAO.class).to(DefaultMigrationStrategyDAO.class);
bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class,
RepositoryManagerProvider.class);
@@ -192,7 +197,7 @@ class ScmServletModule extends ServletModule {
bindDecorated(GroupManager.class, DefaultGroupManager.class,
GroupManagerProvider.class);
bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class);
bind(GroupCollector.class, DefaultGroupCollector.class);
bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class);
// bind sslcontext provider

View File

@@ -48,6 +48,8 @@ import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent;
import javax.inject.Singleton;
/**
* Receives all kinds of events, which affects authorization relevant data and fires an
* {@link AuthorizationChangedEvent} if authorization data has changed.
@@ -55,6 +57,7 @@ import sonia.scm.user.UserModificationEvent;
* @author Sebastian Sdorra
* @since 1.52
*/
@Singleton
@EagerSingleton
public class AuthorizationChangedEventProducer {

View File

@@ -104,7 +104,6 @@ public class BearerRealm extends AuthenticatingRealm
return helper.authenticationInfoBuilder(accessToken.getSubject())
.withCredentials(bt.getCredentials())
.withScope(Scopes.fromClaims(accessToken.getClaims()))
.withGroups(accessToken.getGroups())
.build();
}

View File

@@ -52,17 +52,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupCollector;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.user.User;
import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util;
import java.util.Collection;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -88,19 +89,21 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
/**
* Constructs ...
* @param cacheManager
* @param cacheManager
* @param repositoryDAO
* @param securitySystem
* @param repositoryPermissionProvider
* @param groupCollector
*/
@Inject
public DefaultAuthorizationCollector(CacheManager cacheManager,
RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider)
RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector)
{
this.cache = cacheManager.getCache(CACHE_NAME);
this.repositoryDAO = repositoryDAO;
this.securitySystem = securitySystem;
this.repositoryPermissionProvider = repositoryPermissionProvider;
this.groupCollector = groupCollector;
}
//~--- methods --------------------------------------------------------------
@@ -145,16 +148,16 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
Preconditions.checkNotNull(user, "no user found in principal collection");
GroupNames groupNames = principals.oneByType(GroupNames.class);
Set<String> groups = groupCollector.collect(user.getName());
CacheKey cacheKey = new CacheKey(user.getId(), groupNames);
CacheKey cacheKey = new CacheKey(user.getId(), groups);
AuthorizationInfo info = cache.get(cacheKey);
if (info == null)
{
logger.trace("collect AuthorizationInfo for user {}", user.getName());
info = createAuthorizationInfo(user, groupNames);
info = createAuthorizationInfo(user, groups);
cache.put(cacheKey, info);
}
else if (logger.isTraceEnabled())
@@ -166,7 +169,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
private void collectGlobalPermissions(Builder<String> builder,
final User user, final GroupNames groups)
final User user, final Set<String> groups)
{
Collection<AssignedPermission> globalPermissions =
securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input));
@@ -181,7 +184,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
private void collectRepositoryPermissions(Builder<String> builder, User user,
GroupNames groups)
Set<String> groups)
{
for (Repository repository : repositoryDAO.getAll())
{
@@ -190,7 +193,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
private void collectRepositoryPermissions(Builder<String> builder,
Repository repository, User user, GroupNames groups)
Repository repository, User user, Set<String> groups)
{
Collection<RepositoryPermission> repositoryPermissions = repository.getPermissions();
@@ -245,7 +248,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
.getVerbs();
}
private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) {
private AuthorizationInfo createAuthorizationInfo(User user, Set<String> groups) {
Builder<String> builder = ImmutableSet.builder();
collectGlobalPermissions(builder, user, groups);
@@ -279,7 +282,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
//~--- get methods ----------------------------------------------------------
private boolean isUserPermitted(User user, GroupNames groups,
private boolean isUserPermitted(User user, Set<String> groups,
PermissionObject perm)
{
//J-
@@ -314,7 +317,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
*/
private static class CacheKey
{
private CacheKey(String username, GroupNames groupnames)
private CacheKey(String username, Set<String> groupnames)
{
this.username = username;
this.groupnames = groupnames;
@@ -356,7 +359,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
//~--- fields -------------------------------------------------------------
/** group names */
private final GroupNames groupnames;
private final Set<String> groupnames;
/** username */
private final String username;
@@ -374,4 +377,5 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private final SecuritySystem securitySystem;
private final RepositoryPermissionProvider repositoryPermissionProvider;
private final GroupCollector groupCollector;
}

View File

@@ -34,7 +34,6 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
@@ -45,21 +44,16 @@ import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import sonia.scm.group.GroupNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.Extension;
//~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/**
* Default authorizing realm.
*
@@ -103,6 +97,9 @@ public class DefaultRealm extends AuthorizingRealm
matcher.setPasswordService(service);
setCredentialsMatcher(helper.wrapCredentialsMatcher(matcher));
setAuthenticationTokenClass(UsernamePasswordToken.class);
// we cache in the AuthorizationCollector
setCachingEnabled(false);
}
//~--- methods --------------------------------------------------------------
@@ -149,7 +146,7 @@ public class DefaultRealm extends AuthorizingRealm
LOG.trace("principal does not contain scope information, returning all permissions");
log(principals, info, null);
}
return info;
}
@@ -180,8 +177,6 @@ public class DefaultRealm extends AuthorizingRealm
StringBuilder buffer = new StringBuilder("authorization summary: ");
buffer.append(SEPARATOR).append("username : ").append(collection.getPrimaryPrincipal());
buffer.append(SEPARATOR).append("groups : ");
append(buffer, collection.oneByType(GroupNames.class));
buffer.append(SEPARATOR).append("roles : ");
append(buffer, original.getRoles());
buffer.append(SEPARATOR).append("scope : ");

View File

@@ -40,11 +40,9 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.group.ExternalGroupNames;
import java.time.Clock;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -139,12 +137,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
return this;
}
@Override
public JwtAccessTokenBuilder groups(String... groups) {
Collections.addAll(this.groups, groups);
return this;
}
JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) {
this.refreshExpiration = refreshExpiration;
this.refreshableFor = 0;
@@ -206,16 +198,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
claims.setIssuer(issuer);
}
if (!groups.isEmpty()) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups);
} else {
Subject currentSubject = SecurityUtils.getSubject();
ExternalGroupNames externalGroupNames = currentSubject.getPrincipals().oneByType(ExternalGroupNames.class);
if (externalGroupNames != null) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, externalGroupNames.getCollection().toArray(new String[]{}));
}
}
// sign token and create compact version
String compact = Jwts.builder()
.setClaims(claims)

View File

@@ -7,10 +7,10 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.lifecycle.RestartEvent;
import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.RestartEvent;
import sonia.scm.update.repository.DefaultMigrationStrategyDAO;
import sonia.scm.update.repository.MigrationStrategy;
import sonia.scm.update.repository.MigrationStrategyDao;
import sonia.scm.update.repository.V1Repository;
import sonia.scm.update.repository.XmlRepositoryV1UpdateStep;
import sonia.scm.util.ValidationUtil;
@@ -37,10 +37,10 @@ class MigrationWizardServlet extends HttpServlet {
private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class);
private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep;
private final MigrationStrategyDao migrationStrategyDao;
private final DefaultMigrationStrategyDAO migrationStrategyDao;
@Inject
MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) {
MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, DefaultMigrationStrategyDAO migrationStrategyDao) {
this.repositoryV1UpdateStep = repositoryV1UpdateStep;
this.migrationStrategyDao = migrationStrategyDao;
}
@@ -103,11 +103,12 @@ class MigrationWizardServlet extends HttpServlet {
.forEach(
entry-> {
String id = entry.getId();
String protocol = entry.getType();
String originalName = entry.getOriginalName();
String strategy = req.getParameter("strategy-" + id);
String namespace = req.getParameter("namespace-" + id);
String name = req.getParameter("name-" + id);
migrationStrategyDao.set(id, originalName, MigrationStrategy.valueOf(strategy), namespace, name);
migrationStrategyDao.set(id, protocol, originalName, MigrationStrategy.valueOf(strategy), namespace, name);
}
);

View File

@@ -55,9 +55,10 @@ public class UpdateEngine {
private void execute(UpdateStep updateStep) {
try {
LOG.info("running update step for type {} and version {}",
LOG.info("running update step for type {} and version {} (class {})",
updateStep.getAffectedDataType(),
updateStep.getTargetVersion()
updateStep.getTargetVersion(),
updateStep.getClass().getName()
);
updateStep.doUpdate();
} catch (Exception e) {

View File

@@ -0,0 +1,44 @@
package sonia.scm.update.repository;
import sonia.scm.migration.MigrationDAO;
import sonia.scm.migration.MigrationInfo;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collection;
import java.util.Optional;
import static java.util.stream.Collectors.toList;
@Singleton
public class DefaultMigrationStrategyDAO implements MigrationDAO {
private final RepositoryMigrationPlan plan;
private final ConfigurationStore<RepositoryMigrationPlan> store;
@Inject
public DefaultMigrationStrategyDAO(ConfigurationStoreFactory storeFactory) {
store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build();
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
}
public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) {
return plan.get(id);
}
public void set(String repositoryId, String protocol, String originalName, MigrationStrategy strategy, String newNamespace, String newName) {
plan.set(repositoryId, protocol, originalName, strategy, newNamespace, newName);
store.set(plan);
}
@Override
public Collection<MigrationInfo> getAll() {
return plan
.getEntries()
.stream()
.map(e -> new MigrationInfo(e.getRepositoryId(), e.getProtocol(), e.getOriginalName(), e.getNewNamespace(), e.getNewName()))
.collect(toList());
}
}

View File

@@ -1,30 +0,0 @@
package sonia.scm.update.repository;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class MigrationStrategyDao {
private final RepositoryMigrationPlan plan;
private final ConfigurationStore<RepositoryMigrationPlan> store;
@Inject
public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) {
store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build();
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
}
public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) {
return plan.get(id);
}
public void set(String repositoryId, String originalName, MigrationStrategy strategy, String newNamespace, String newName) {
plan.set(repositoryId, originalName, strategy, newNamespace, newName);
store.set(plan);
}
}

View File

@@ -4,6 +4,8 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -29,14 +31,18 @@ class RepositoryMigrationPlan {
.findFirst();
}
public void set(String repositoryId, String originalName, MigrationStrategy strategy, String newNamespace, String newName) {
public Collection<RepositoryMigrationEntry> getEntries() {
return Collections.unmodifiableList(entries);
}
public void set(String repositoryId, String protocol, String originalName, MigrationStrategy strategy, String newNamespace, String newName) {
Optional<RepositoryMigrationEntry> entry = get(repositoryId);
if (entry.isPresent()) {
entry.get().setStrategy(strategy);
entry.get().setNewNamespace(newNamespace);
entry.get().setNewName(newName);
} else {
entries.add(new RepositoryMigrationEntry(repositoryId, originalName, strategy, newNamespace, newName));
entries.add(new RepositoryMigrationEntry(repositoryId, protocol, originalName, strategy, newNamespace, newName));
}
}
@@ -45,6 +51,7 @@ class RepositoryMigrationPlan {
static class RepositoryMigrationEntry {
private String repositoryId;
private String protocol;
private String originalName;
private MigrationStrategy dataMigrationStrategy;
private String newNamespace;
@@ -53,14 +60,23 @@ class RepositoryMigrationPlan {
RepositoryMigrationEntry() {
}
RepositoryMigrationEntry(String repositoryId, String originalName, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) {
RepositoryMigrationEntry(String repositoryId, String protocol, String originalName, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) {
this.repositoryId = repositoryId;
this.protocol = protocol;
this.originalName = originalName;
this.dataMigrationStrategy = dataMigrationStrategy;
this.newNamespace = newNamespace;
this.newName = newName;
}
public String getRepositoryId() {
return repositoryId;
}
public String getProtocol() {
return protocol;
}
public String getOriginalName() {
return originalName;
}

View File

@@ -49,7 +49,7 @@ import static sonia.scm.version.Version.parse;
* <li>a new entry in the new <code>repository-paths.xml</code> database is written,</li>
* <li>the data directory is moved or copied to a SCM v2 consistent directory. How this is done
* can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in
* a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link MigrationStrategyDao}),
* a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link DefaultMigrationStrategyDAO}),
* and
* <li>the new <code>metadata.xml</code> file is created.</li>
* </ul>
@@ -63,7 +63,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDao;
private final MigrationStrategyDao migrationStrategyDao;
private final DefaultMigrationStrategyDAO migrationStrategyDao;
private final Injector injector;
private final ConfigurationEntryStore<V1Properties> propertyStore;
@@ -71,7 +71,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
public XmlRepositoryV1UpdateStep(
SCMContextProvider contextProvider,
XmlRepositoryDAO repositoryDao,
MigrationStrategyDao migrationStrategyDao,
DefaultMigrationStrategyDAO migrationStrategyDao,
Injector injector,
ConfigurationEntryStoreFactory configurationEntryStoreFactory
) {

View File

@@ -21,7 +21,10 @@ import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Optional.ofNullable;
import static sonia.scm.version.Version.parse;
@@ -29,6 +32,8 @@ import static sonia.scm.version.Version.parse;
@Extension
public class XmlSecurityV1UpdateStep implements UpdateStep {
private static final Pattern v1PermissionPattern = Pattern.compile("^repository:\\*:(READ|WRITE|OWNER)$");
private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class);
private final SCMContextProvider contextProvider;
@@ -46,6 +51,50 @@ public class XmlSecurityV1UpdateStep implements UpdateStep {
forAllAdmins(user -> createSecurityEntry(user, false, securityStore),
group -> createSecurityEntry(group, true, securityStore));
mapV1Permissions(securityStore);
}
private void mapV1Permissions(ConfigurationEntryStore<AssignedPermission> securityStore) throws JAXBException {
Path v1SecurityFile = determineConfigDirectory().resolve("securityV1" + StoreConstants.FILE_EXTENSION);
if (!v1SecurityFile.toFile().exists()) {
LOG.info("no v1 file for security found");
return;
}
JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Security.class);
V1Security v1Security = (V1Security) jaxbContext.createUnmarshaller().unmarshal(v1SecurityFile.toFile());
v1Security.entries.forEach(assignedPermission -> {
Matcher matcher = v1PermissionPattern.matcher(assignedPermission.value.permission);
if (matcher.matches()) {
String newPermission = convertRole(matcher.group(1));
securityStore.put(new AssignedPermission(
assignedPermission.value.name,
Boolean.parseBoolean(assignedPermission.value.groupPermission),
newPermission
));
}
});
}
private String convertRole(String role) {
String newPermission;
switch (role) {
case "OWNER":
newPermission = "repository:*";
break;
case "WRITE":
newPermission = "repository:read,pull,push:*";
break;
case "READ":
newPermission = "repository:read,pull:*";
break;
default:
newPermission = "";
}
return newPermission;
}
private void forAllAdmins(Consumer<String> userConsumer, Consumer<String> groupConsumer) throws JAXBException {
@@ -70,10 +119,9 @@ public class XmlSecurityV1UpdateStep implements UpdateStep {
Arrays.stream(entries.split(",")).forEach(consumer);
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
return parse("2.0.1");
}
@Override
@@ -102,4 +150,29 @@ public class XmlSecurityV1UpdateStep implements UpdateStep {
@XmlElement(name = "admin-groups")
private String adminGroups;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "configuration")
private static class V1Security {
@XmlElement(name = "entry")
private List<Entry> entries;
}
@XmlAccessorType(XmlAccessType.FIELD)
private static class Entry {
@XmlElement(name = "key")
private String key;
@XmlElement(name = "value")
private Value value;
}
@XmlAccessorType(XmlAccessType.FIELD)
private static class Value {
@XmlElement(name = "permission")
String permission;
@XmlElement(name = "name")
String name;
@XmlElement(name = "group-permission")
String groupPermission;
}
}

View File

@@ -57,7 +57,8 @@ public class XmlUserV1UpdateStep implements UpdateStep {
@Override
public void doUpdate() throws JAXBException {
Optional<Path> v1UsersFile = determineV1File();
Optional<Path> v1UsersFile = determineV1File("users");
determineV1File("security");
if (!v1UsersFile.isPresent()) {
LOG.info("no v1 file for users found");
return;
@@ -107,17 +108,17 @@ public class XmlUserV1UpdateStep implements UpdateStep {
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
}
private Optional<Path> determineV1File() {
Path existingUsersFile = resolveConfigFile("users");
Path usersV1File = resolveConfigFile("usersV1");
if (existingUsersFile.toFile().exists()) {
private Optional<Path> determineV1File(String filename) {
Path existingFile = resolveConfigFile(filename);
Path v1File = resolveConfigFile(filename + "V1");
if (existingFile.toFile().exists()) {
try {
Files.move(existingUsersFile, usersV1File);
Files.move(existingFile, v1File);
} catch (IOException e) {
throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath());
throw new UpdateException("could not move old " + filename + " file to " + v1File.toAbsolutePath());
}
LOG.info("moved old users file to {}", usersV1File.toAbsolutePath());
return of(usersV1File);
LOG.info("moved old " + filename + " file to {}", v1File.toAbsolutePath());
return of(v1File);
}
return empty();
}

View File

@@ -0,0 +1,22 @@
package sonia.scm.web.filter;
import sonia.scm.Priority;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
import sonia.scm.web.UserAgentParser;
import sonia.scm.web.WebTokenGenerator;
import sonia.scm.web.protocol.HttpProtocolServlet;
import javax.inject.Inject;
import java.util.Set;
@Priority(Filters.PRIORITY_AUTHENTICATION)
@WebElement(value = HttpProtocolServlet.PATTERN)
public class DefaultHttpProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase {
@Inject
public DefaultHttpProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set<WebTokenGenerator> tokenGenerators, UserAgentParser userAgentParser) {
super(configuration, tokenGenerators, userAgentParser);
}
}

View File

@@ -1,46 +0,0 @@
package sonia.scm.web.filter;
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.UserAgent;
import sonia.scm.web.UserAgentParser;
import sonia.scm.web.WebTokenGenerator;
import sonia.scm.web.protocol.HttpProtocolServlet;
import javax.inject.Inject;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
@Priority(Filters.PRIORITY_AUTHENTICATION)
@WebElement(value = HttpProtocolServlet.PATTERN)
public class HttpProtocolServletAuthenticationFilter extends AuthenticationFilter {
private final UserAgentParser userAgentParser;
@Inject
public HttpProtocolServletAuthenticationFilter(
ScmConfiguration configuration,
Set<WebTokenGenerator> tokenGenerators,
UserAgentParser userAgentParser) {
super(configuration, tokenGenerators);
this.userAgentParser = userAgentParser;
}
@Override
protected void handleUnauthorized(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
UserAgent userAgent = userAgentParser.parse(request);
if (userAgent.isBrowser()) {
// we can proceed the filter chain because the HttpProtocolServlet will render the ui if the client is a browser
chain.doFilter(request, response);
} else {
HttpUtil.sendUnauthorized(request, response);
}
}
}

View File

@@ -16,7 +16,6 @@ import sonia.scm.repository.spi.HttpScmProtocol;
import sonia.scm.web.UserAgent;
import sonia.scm.web.UserAgentParser;
import javax.inject.Provider;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@@ -33,17 +32,15 @@ public class HttpProtocolServlet extends HttpServlet {
public static final String PATTERN = PATH + "/*";
private final RepositoryServiceFactory serviceFactory;
private final Provider<HttpServletRequest> requestProvider;
private final NamespaceAndNameFromPathExtractor pathExtractor;
private final PushStateDispatcher dispatcher;
private final UserAgentParser userAgentParser;
@Inject
public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, Provider<HttpServletRequest> requestProvider, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) {
public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, NamespaceAndNameFromPathExtractor pathExtractor, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) {
this.serviceFactory = serviceFactory;
this.requestProvider = requestProvider;
this.pathExtractor = pathExtractor;
this.dispatcher = dispatcher;
this.userAgentParser = userAgentParser;
}
@@ -55,9 +52,8 @@ public class HttpProtocolServlet extends HttpServlet {
log.trace("dispatch browser request for user agent {}", userAgent);
dispatcher.dispatch(request, response, request.getRequestURI());
} else {
String pathInfo = request.getPathInfo();
Optional<NamespaceAndName> namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(pathInfo);
Optional<NamespaceAndName> namespaceAndName = pathExtractor.fromUri(pathInfo);
if (namespaceAndName.isPresent()) {
service(request, response, namespaceAndName.get());
} else {
@@ -69,7 +65,7 @@ public class HttpProtocolServlet extends HttpServlet {
private void service(HttpServletRequest req, HttpServletResponse resp, NamespaceAndName namespaceAndName) throws IOException, ServletException {
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
requestProvider.get().setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository());
req.setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository());
HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
protocol.serve(req, resp, getServletConfig());
} catch (NotFoundException e) {

View File

@@ -1,18 +1,31 @@
package sonia.scm.web.protocol;
import sonia.scm.Type;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.util.HttpUtil;
import javax.inject.Inject;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.Optional.empty;
import static java.util.Optional.of;
final class NamespaceAndNameFromPathExtractor {
private NamespaceAndNameFromPathExtractor() {}
private final Set<String> types;
static Optional<NamespaceAndName> fromUri(String uri) {
@Inject
public NamespaceAndNameFromPathExtractor(RepositoryManager repositoryManager) {
this.types = repositoryManager.getConfiguredTypes()
.stream()
.map(Type::getName)
.collect(Collectors.toSet());
}
Optional<NamespaceAndName> fromUri(String uri) {
if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) {
uri = uri.substring(1);
}
@@ -30,12 +43,13 @@ final class NamespaceAndNameFromPathExtractor {
}
String name = uri.substring(endOfNamespace + 1, nameIndex);
int nameDotIndex = name.indexOf('.');
int nameDotIndex = name.lastIndexOf('.');
if (nameDotIndex >= 0) {
return of(new NamespaceAndName(namespace, name.substring(0, nameDotIndex)));
} else {
return of(new NamespaceAndName(namespace, name));
String suffix = name.substring(nameDotIndex + 1);
if (types.contains(suffix)) {
name = name.substring(0, nameDotIndex);
}
}
return of(new NamespaceAndName(namespace, name));
}
}

View File

@@ -38,7 +38,6 @@ package sonia.scm.web.security;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
@@ -46,21 +45,17 @@ import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.group.GroupNames;
import sonia.scm.security.Role;
import sonia.scm.user.User;
import sonia.scm.util.AssertUtil;
//~--- JDK imports ------------------------------------------------------------
import javax.xml.bind.JAXB;
import java.net.URL;
import javax.xml.bind.JAXB;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -161,7 +156,6 @@ public class DefaultAdministrationContext implements AdministrationContext
collection.add(adminUser.getId(), REALM);
collection.add(adminUser, REALM);
collection.add(new GroupNames(), REALM);
collection.add(AdministrationContextMarker.MARKER, REALM);
return collection;