This commit is contained in:
Mohamed Karray
2018-10-02 13:20:55 +02:00
42 changed files with 949 additions and 66 deletions

View File

@@ -0,0 +1,14 @@
package sonia.scm.api.v2;
public final class ValidationConstraints {
private ValidationConstraints() {}
/**
* A user or group name should not start with <code>@</code> or a whitespace
* and it not contains whitespaces
* and the characters: . - _ @ are allowed
*/
public static final String USER_GROUP_PATTERN = "^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$";
}

View File

@@ -13,6 +13,8 @@ import java.time.Instant;
import java.util.List;
import java.util.Map;
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
@Getter @Setter @NoArgsConstructor
public class GroupDto extends HalRepresentation {
@@ -20,7 +22,7 @@ public class GroupDto extends HalRepresentation {
private String description;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified;
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
@Pattern(regexp = USER_GROUP_PATTERN)
private String name;
@NotEmpty
private String type;

View File

@@ -0,0 +1,16 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
@Getter
public class IndexDto extends HalRepresentation {
private final String version;
IndexDto(String version, Links links) {
super(links);
this.version = version;
}
}

View File

@@ -0,0 +1,50 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.apache.shiro.SecurityUtils;
import sonia.scm.SCMContextProvider;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.group.GroupPermissions;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
public class IndexDtoGenerator {
private final ResourceLinks resourceLinks;
private final SCMContextProvider scmContextProvider;
@Inject
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) {
this.resourceLinks = resourceLinks;
this.scmContextProvider = scmContextProvider;
}
public IndexDto generate() {
Links.Builder builder = Links.linkingTo();
builder.self(resourceLinks.index().self());
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
if (SecurityUtils.getSubject().isAuthenticated()) {
builder.single(
link("me", resourceLinks.me().self()),
link("logout", resourceLinks.authentication().logout())
);
if (UserPermissions.list().isPermitted()) {
builder.single(link("users", resourceLinks.userCollection().self()));
}
if (GroupPermissions.list().isPermitted()) {
builder.single(link("groups", resourceLinks.groupCollection().self()));
}
if (ConfigurationPermissions.list().isPermitted()) {
builder.single(link("config", resourceLinks.config().self()));
}
builder.single(link("repositories", resourceLinks.repositoryCollection().self()));
} else {
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
}
return new IndexDto(scmContextProvider.getVersion(), builder.build());
}
}

View File

@@ -0,0 +1,29 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path(IndexResource.INDEX_PATH_V2)
public class IndexResource {
public static final String INDEX_PATH_V2 = "v2/";
private final IndexDtoGenerator indexDtoGenerator;
@Inject
public IndexResource(IndexDtoGenerator indexDtoGenerator) {
this.indexDtoGenerator = indexDtoGenerator;
}
@GET
@Path("")
@Produces(VndMediaType.INDEX)
@TypeHint(IndexDto.class)
public IndexDto getIndex() {
return indexDtoGenerator.generate();
}
}

View File

@@ -4,15 +4,20 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter @ToString
import javax.validation.constraints.Pattern;
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
@Getter @Setter @ToString @NoArgsConstructor
public class PermissionDto extends HalRepresentation {
public static final String GROUP_PREFIX = "@";
@JsonInclude(JsonInclude.Include.NON_NULL)
@Pattern(regexp = USER_GROUP_PATTERN)
private String name;
/**
@@ -28,9 +33,6 @@ public class PermissionDto extends HalRepresentation {
private boolean groupPermission = false;
public PermissionDto() {
}
public PermissionDto(String permissionName, boolean groupPermission) {
name = permissionName;
this.groupPermission = groupPermission;

View File

@@ -16,6 +16,7 @@ import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
@@ -70,7 +71,7 @@ public class PermissionRootResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION)
@Path("")
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws Exception {
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) throws AlreadyExistsException, NotFoundException {
log.info("try to add new permission: {}", permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
@@ -156,7 +157,7 @@ public class PermissionRootResource {
public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName,
PermissionDto permission) throws NotFoundException, AlreadyExistsException {
@Valid PermissionDto permission) throws NotFoundException, AlreadyExistsException {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();

View File

@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName;
import javax.inject.Inject;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
class ResourceLinks {
@@ -441,7 +440,6 @@ class ResourceLinks {
}
}
public UIPluginLinks uiPlugin() {
return new UIPluginLinks(scmPathInfoStore.get());
}
@@ -473,4 +471,45 @@ class ResourceLinks {
return uiPluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href();
}
}
public AuthenticationLinks authentication() {
return new AuthenticationLinks(scmPathInfoStore.get());
}
static class AuthenticationLinks {
private final LinkBuilder loginLinkBuilder;
AuthenticationLinks(ScmPathInfo pathInfo) {
this.loginLinkBuilder = new LinkBuilder(pathInfo, AuthenticationResource.class);
}
String formLogin() {
return loginLinkBuilder.method("authenticateViaForm").parameters().href();
}
String jsonLogin() {
return loginLinkBuilder.method("authenticateViaJSONBody").parameters().href();
}
String logout() {
return loginLinkBuilder.method("logout").parameters().href();
}
}
public IndexLinks index() {
return new IndexLinks(scmPathInfoStore.get());
}
static class IndexLinks {
private final LinkBuilder indexLinkBuilder;
IndexLinks(ScmPathInfo pathInfo) {
indexLinkBuilder = new LinkBuilder(pathInfo, IndexResource.class);
}
String self() {
return indexLinkBuilder.method("getIndex").parameters().href();
}
}
}

View File

@@ -13,6 +13,8 @@ import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.Map;
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
@NoArgsConstructor @Getter @Setter
public class UserDto extends HalRepresentation {
private boolean active;
@@ -24,7 +26,7 @@ public class UserDto extends HalRepresentation {
private Instant lastModified;
@NotEmpty @Email
private String mail;
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
@Pattern(regexp = USER_GROUP_PATTERN)
private String name;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String password;

View File

@@ -84,7 +84,7 @@ public class SecurityFilter extends HttpFilter
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if (!SecurityRequests.isAuthenticationRequest(request))
if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request))
{
Subject subject = SecurityUtils.getSubject();
if (hasPermission(subject))

View File

@@ -56,6 +56,7 @@ import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.user.User;
import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util;
import java.util.List;
@@ -74,7 +75,7 @@ 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 = "*";
@@ -88,7 +89,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
LoggerFactory.getLogger(DefaultAuthorizationCollector.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
@@ -209,7 +210,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
String perm = permission.getType().getPermissionPrefix().concat(repository.getId());
if (logger.isTraceEnabled())
{
logger.trace("add repository permission {} for user {} at repository {}",
logger.trace("add repository permission {} for user {} at repository {}",
perm, user.getName(), repository.getName());
}
@@ -254,6 +255,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
collectGlobalPermissions(builder, user, groups);
collectRepositoryPermissions(builder, user, groups);
builder.add(canReadOwnUser(user));
permissions = builder.build();
}
@@ -262,6 +264,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return info;
}
private String canReadOwnUser(User user) {
return UserPermissions.read(user.getName()).asShiroString();
}
//~--- get methods ----------------------------------------------------------
private boolean isUserPermitted(User user, GroupNames groups,
@@ -272,7 +278,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|| ((!perm.isGroupPermission()) && user.getName().equals(perm.getName()));
//J+
}
@Subscribe
public void invalidateCache(AuthorizationChangedEvent event) {
if (event.isEveryUserAffected()) {
@@ -281,12 +287,12 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
invalidateCache();
}
}
private void invalidateUserCache(final String username) {
logger.info("invalidate cache for user {}, because of a received authorization event", username);
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
}
private void invalidateCache() {
logger.info("invalidate cache, because of a received authorization event");
cache.clear();

View File

@@ -11,6 +11,7 @@ import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
public final class SecurityRequests {
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile(REST_API_PATH + "(?:/v2)?/auth/access_token");
private static final Pattern URI_INDEX_PATTERN = Pattern.compile(REST_API_PATH + "/v2/?");
private SecurityRequests() {}
@@ -23,4 +24,13 @@ public final class SecurityRequests {
return URI_LOGIN_PATTERN.matcher(uri).matches();
}
public static boolean isIndexRequest(HttpServletRequest request) {
String uri = request.getRequestURI().substring(request.getContextPath().length());
return isIndexRequest(uri);
}
public static boolean isIndexRequest(String uri) {
return URI_INDEX_PATTERN.matcher(uri).matches();
}
}

View File

@@ -99,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
throws IOException, ServletException
{
// skip filter on login resource
if (SecurityRequests.isAuthenticationRequest(request))
if (SecurityRequests.isAuthenticationRequest(request) )
{
chain.doFilter(request, response);
}