Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-01-21 15:09:23 +01:00
27 changed files with 824 additions and 386 deletions

View File

@@ -8,7 +8,6 @@ public class MapperModule extends AbstractModule {
@Override
protected void configure() {
bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass());
bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass());
bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass());
bind(UserCollectionToDtoMapper.class);
@@ -46,6 +45,7 @@ public class MapperModule extends AbstractModule {
bind(MergeResultToDtoMapper.class).to(Mappers.getMapper(MergeResultToDtoMapper.class).getClass());
// no mapstruct required
bind(MeDtoFactory.class);
bind(UIPluginDtoMapper.class);
bind(UIPluginDtoCollectionMapper.class);

View File

@@ -0,0 +1,26 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class MeDto extends HalRepresentation {
private String name;
private String displayName;
private String mail;
private List<String> groups;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
}

View File

@@ -0,0 +1,81 @@
package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableList;
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.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.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
public class MeDtoFactory extends LinkAppenderMapper {
private final ResourceLinks resourceLinks;
private final UserManager userManager;
@Inject
public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) {
this.resourceLinks = resourceLinks;
this.userManager = userManager;
}
public MeDto create() {
PrincipalCollection principals = getPrincipalCollection();
MeDto dto = new MeDto();
User user = principals.oneByType(User.class);
mapUserProperties(user, dto);
mapGroups(principals, dto);
appendLinks(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 mapUserProperties(User user, MeDto dto) {
dto.setName(user.getName());
dto.setDisplayName(user.getDisplayName());
dto.setMail(user.getMail());
}
private PrincipalCollection getPrincipalCollection() {
Subject subject = SecurityUtils.getSubject();
return subject.getPrincipals();
}
private void appendLinks(User user, MeDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
}
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user);
target.add(linksBuilder.build());
}
}

View File

@@ -3,14 +3,11 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@@ -28,20 +25,18 @@ import javax.ws.rs.core.UriInfo;
*/
@Path(MeResource.ME_PATH_V2)
public class MeResource {
public static final String ME_PATH_V2 = "v2/me/";
private final MeToUserDtoMapper meToUserDtoMapper;
static final String ME_PATH_V2 = "v2/me/";
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
private final MeDtoFactory meDtoFactory;
private final UserManager userManager;
private final PasswordService passwordService;
@Inject
public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) {
this.meToUserDtoMapper = meToUserDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
public MeResource(MeDtoFactory meDtoFactory, UserManager userManager, PasswordService passwordService) {
this.meDtoFactory = meDtoFactory;
this.userManager = userManager;
this.passwordService = passwordService;
this.userManager = manager;
}
/**
@@ -49,17 +44,15 @@ public class MeResource {
*/
@GET
@Path("")
@Produces(VndMediaType.USER)
@TypeHint(UserDto.class)
@Produces(VndMediaType.ME)
@TypeHint(MeDto.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@Context Request request, @Context UriInfo uriInfo) {
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.get(id, meToUserDtoMapper::map);
return Response.ok(meDtoFactory.create()).build();
}
/**
@@ -75,7 +68,10 @@ public class MeResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PASSWORD_CHANGE)
public Response changePassword(@Valid PasswordChangeDto passwordChange) {
userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChange.getOldPassword()), passwordService.encryptPassword(passwordChange.getNewPassword()));
userManager.changePasswordForLoggedInUser(
passwordService.encryptPassword(passwordChange.getOldPassword()),
passwordService.encryptPassword(passwordChange.getNewPassword())
);
return Response.noContent().build();
}
}

View File

@@ -1,45 +0,0 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class MeToUserDtoMapper extends UserToUserDtoMapper {
@Inject
private UserManager userManager;
@Inject
private ResourceLinks resourceLinks;
@Override
@AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
}
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user);
target.add(linksBuilder.build());
}
}

View File

@@ -101,11 +101,11 @@ public class BearerRealm extends AuthenticatingRealm
BearerToken bt = (BearerToken) token;
AccessToken accessToken = tokenResolver.resolve(bt);
return helper.getAuthenticationInfo(
accessToken.getSubject(),
bt.getCredentials(),
Scopes.fromClaims(accessToken.getClaims())
);
return helper.authenticationInfoBuilder(accessToken.getSubject())
.withCredentials(bt.getCredentials())
.withScope(Scopes.fromClaims(accessToken.getClaims()))
.withGroups(accessToken.getGroups())
.build();
}
}

View File

@@ -51,6 +51,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.Extension;
@@ -75,9 +76,6 @@ 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 = "*";
@@ -97,14 +95,16 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
*
*
*
* @param configuration
* @param cacheManager
* @param repositoryDAO
* @param securitySystem
*/
@Inject
public DefaultAuthorizationCollector(CacheManager cacheManager,
RepositoryDAO repositoryDAO, SecuritySystem securitySystem)
public DefaultAuthorizationCollector(ScmConfiguration configuration, CacheManager cacheManager,
RepositoryDAO repositoryDAO, SecuritySystem securitySystem)
{
this.configuration = configuration;
this.cache = cacheManager.getCache(CACHE_NAME);
this.repositoryDAO = repositoryDAO;
this.securitySystem = securitySystem;
@@ -238,7 +238,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
Set<String> roles;
Set<String> permissions;
if (user.isAdmin())
if (isAdmin(user, groups))
{
if (logger.isDebugEnabled())
{
@@ -269,6 +269,37 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return info;
}
private boolean isAdmin(User user, GroupNames groups) {
boolean admin = user.isAdmin();
if (admin) {
logger.debug("user {} is marked as admin, because of the user flag", user.getName());
return true;
}
if (isUserAdminInConfiguration(user)) {
logger.debug("user {} is marked as admin, because of the admin user configuration", user.getName());
return true;
}
return isUserAdminInGroupConfiguration(user, groups);
}
private boolean isUserAdminInGroupConfiguration(User user, GroupNames groups) {
Set<String> adminGroups = configuration.getAdminGroups();
if (adminGroups != null && groups != null) {
for (String group : groups) {
if (adminGroups.contains(group)) {
logger.debug("user {} is marked as admin, because of the admin group configuration for group {}", user.getName(), group);
return true;
}
}
}
return false;
}
private boolean isUserAdminInConfiguration(User user) {
Set<String> adminUsers = configuration.getAdminUsers();
return adminUsers != null && adminUsers.contains(user.getName());
}
private String getGroupAutocompletePermission() {
return GroupPermissions.autocomplete().asShiroString();
}
@@ -372,6 +403,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
//~--- fields ---------------------------------------------------------------
private final ScmConfiguration configuration;
/** authorization cache */
private final Cache<CacheKey, AuthorizationInfo> cache;

View File

@@ -30,12 +30,15 @@
*/
package sonia.scm.security;
import com.google.common.collect.ImmutableSet;
import io.jsonwebtoken.Claims;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static java.util.Optional.ofNullable;
@@ -49,6 +52,8 @@ public final class JwtAccessToken implements AccessToken {
public static final String REFRESHABLE_UNTIL_CLAIM_KEY = "scm-manager.refreshExpiration";
public static final String PARENT_TOKEN_ID_CLAIM_KEY = "scm-manager.parentTokenId";
public static final String GROUPS_CLAIM_KEY = "scm-manager.groups";
private final Claims claims;
private final String compact;
@@ -103,6 +108,16 @@ public final class JwtAccessToken implements AccessToken {
return Optional.ofNullable(claims.get(key));
}
@Override
@SuppressWarnings("unchecked")
public Set<String> getGroups() {
Iterable<String> groups = claims.get(GROUPS_CLAIM_KEY, Iterable.class);
if (groups != null) {
return ImmutableSet.copyOf(groups);
}
return ImmutableSet.of();
}
@Override
public String compact() {
return compact;

View File

@@ -39,9 +39,12 @@ import io.jsonwebtoken.SignatureAlgorithm;
import java.time.Clock;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
@@ -74,6 +77,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
private Instant refreshExpiration;
private String parentKeyId;
private Scope scope = Scope.empty();
private Set<String> groups = new HashSet<>();
private final Map<String,Object> custom = Maps.newHashMap();
@@ -134,6 +138,12 @@ 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;
@@ -195,6 +205,10 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
claims.setIssuer(issuer);
}
if (!groups.isEmpty()) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups);
}
// sign token and create compact version
String compact = Jwts.builder()
.setClaims(claims)