Add role attribute to repository permission

This commit is contained in:
René Pfeuffer
2019-05-03 07:59:18 +02:00
parent ad51a6fa27
commit 9109efe2f1
5 changed files with 166 additions and 17 deletions

View File

@@ -50,6 +50,7 @@ import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet; import static java.util.Collections.unmodifiableSet;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -73,6 +74,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
private String name; private String name;
@XmlElement(name = "verb") @XmlElement(name = "verb")
private Set<String> verbs; private Set<String> verbs;
private String role;
/** /**
* This constructor exists for mapstruct and JAXB, only -- <b>do not use this in "normal" code</b>. * This constructor exists for mapstruct and JAXB, only -- <b>do not use this in "normal" code</b>.
@@ -87,6 +89,15 @@ public class RepositoryPermission implements PermissionObject, Serializable
{ {
this.name = name; this.name = name;
this.verbs = new LinkedHashSet<>(verbs); this.verbs = new LinkedHashSet<>(verbs);
this.role = null;
this.groupPermission = groupPermission;
}
public RepositoryPermission(String name, String role, boolean groupPermission)
{
this.name = name;
this.verbs = emptySet();
this.role = role;
this.groupPermission = groupPermission; this.groupPermission = groupPermission;
} }
@@ -116,8 +127,9 @@ public class RepositoryPermission implements PermissionObject, Serializable
final RepositoryPermission other = (RepositoryPermission) obj; final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name) return Objects.equal(name, other.name)
&& verbs.containsAll(other.verbs)
&& verbs.size() == other.verbs.size() && verbs.size() == other.verbs.size()
&& verbs.containsAll(other.verbs)
&& Objects.equal(role, other.role)
&& Objects.equal(groupPermission, other.groupPermission); && Objects.equal(groupPermission, other.groupPermission);
} }
@@ -132,7 +144,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
{ {
// Normally we do not have a log of repository permissions having the same size of verbs, but different content. // Normally we do not have a log of repository permissions having the same size of verbs, but different content.
// Therefore we do not use the verbs themselves for the hash code but only the number of verbs. // Therefore we do not use the verbs themselves for the hash code but only the number of verbs.
return Objects.hashCode(name, verbs.size(), groupPermission); return Objects.hashCode(name, verbs.size(), role, groupPermission);
} }
@@ -173,6 +185,16 @@ public class RepositoryPermission implements PermissionObject, Serializable
return verbs == null ? emptyList() : Collections.unmodifiableSet(verbs); return verbs == null ? emptyList() : Collections.unmodifiableSet(verbs);
} }
/**
* Returns the role of the permission.
*
*
* @return role of the permission
*/
public String getRole() {
return role;
}
/** /**
* Returns true if the permission is a permission which affects a group. * Returns true if the permission is a permission which affects a group.
* *
@@ -192,7 +214,8 @@ public class RepositoryPermission implements PermissionObject, Serializable
* @throws IllegalStateException when modified after the value has been set once. * @throws IllegalStateException when modified after the value has been set once.
* *
* @deprecated Do not use this for "normal" code. * @deprecated Do not use this for "normal" code.
* Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)}
* or {@link RepositoryPermission#RepositoryPermission(String, String, boolean)} instead.
*/ */
@Deprecated @Deprecated
public void setGroupPermission(boolean groupPermission) public void setGroupPermission(boolean groupPermission)
@@ -208,7 +231,8 @@ public class RepositoryPermission implements PermissionObject, Serializable
* @throws IllegalStateException when modified after the value has been set once. * @throws IllegalStateException when modified after the value has been set once.
* *
* @deprecated Do not use this for "normal" code. * @deprecated Do not use this for "normal" code.
* Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)}
* or {@link RepositoryPermission#RepositoryPermission(String, String, boolean)} instead.
*/ */
@Deprecated @Deprecated
public void setName(String name) public void setName(String name)
@@ -219,6 +243,22 @@ public class RepositoryPermission implements PermissionObject, Serializable
this.name = name; this.name = name;
} }
/**
* Use this for creation only. This will throw an {@link IllegalStateException} when modified.
* @throws IllegalStateException when modified after the value has been set once.
*
* @deprecated Do not use this for "normal" code.
* Use {@link RepositoryPermission#RepositoryPermission(String, String, boolean)} instead.
*/
@Deprecated
public void setRole(String role)
{
if (this.role != null) {
throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT);
}
this.role = role;
}
/** /**
* Use this for creation only. This will throw an {@link IllegalStateException} when modified. * Use this for creation only. This will throw an {@link IllegalStateException} when modified.
* @throws IllegalStateException when modified after the value has been set once. * @throws IllegalStateException when modified after the value has been set once.
@@ -232,6 +272,6 @@ public class RepositoryPermission implements PermissionObject, Serializable
if (this.verbs != null) { if (this.verbs != null) {
throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT);
} }
this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); this.verbs = verbs == null? emptySet(): unmodifiableSet(new LinkedHashSet<>(verbs));
} }
} }

View File

@@ -0,0 +1,23 @@
package sonia.scm.api.v2.resources;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = EitherRoleOrVerbsValidator.class)
@Documented
public @interface EitherRoleOrVerbs {
String message() default "permission must either have a role or a not empty set of verbs";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,30 @@
package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EitherRoleOrVerbsValidator implements ConstraintValidator<EitherRoleOrVerbs, RepositoryPermissionDto> {
private static final Logger LOG = LoggerFactory.getLogger(EitherRoleOrVerbsValidator.class);
@Override
public void initialize(EitherRoleOrVerbs constraintAnnotation) {
}
@Override
public boolean isValid(RepositoryPermissionDto object, ConstraintValidatorContext constraintContext) {
if (Strings.isNullOrEmpty(object.getRole())) {
boolean result = object.getVerbs() != null && !object.getVerbs().isEmpty();
LOG.trace("Validation result for permission with empty or no role: {}", result);
return result;
} else {
boolean result = object.getVerbs() == null || object.getVerbs().isEmpty();
LOG.trace("Validation result for permission with non empty role: {}", result);
return result;
}
}
}

View File

@@ -14,6 +14,7 @@ import javax.validation.constraints.Pattern;
import java.util.Collection; import java.util.Collection;
@Getter @Setter @ToString @NoArgsConstructor @Getter @Setter @ToString @NoArgsConstructor
@EitherRoleOrVerbs
public class RepositoryPermissionDto extends HalRepresentation { public class RepositoryPermissionDto extends HalRepresentation {
public static final String GROUP_PREFIX = "@"; public static final String GROUP_PREFIX = "@";
@@ -21,9 +22,11 @@ public class RepositoryPermissionDto extends HalRepresentation {
@Pattern(regexp = ValidationUtil.REGEX_NAME) @Pattern(regexp = ValidationUtil.REGEX_NAME)
private String name; private String name;
@NotEmpty @NoBlankStrings
private Collection<String> verbs; private Collection<String> verbs;
private String role;
private boolean groupPermission = false; private boolean groupPermission = false;
public RepositoryPermissionDto(String permissionName, boolean groupPermission) { public RepositoryPermissionDto(String permissionName, boolean groupPermission) {

View File

@@ -2,8 +2,6 @@ package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.inject.util.Providers; import com.google.inject.util.Providers;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
@@ -21,7 +19,6 @@ import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@@ -35,6 +32,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.ws.rs.HttpMethod;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
@@ -64,11 +62,6 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX; import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
@Slf4j @Slf4j
@SubjectAware(
username = "trillian",
password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAMESPACE = "repo_namespace";
private static final String REPOSITORY_NAME = "repo"; private static final String REPOSITORY_NAME = "repo";
@@ -114,9 +107,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
private Dispatcher dispatcher; private Dispatcher dispatcher;
@Rule
public ShiroRule shiro = new ShiroRule();
@Mock @Mock
private RepositoryManager repositoryManager; private RepositoryManager repositoryManager;
@@ -363,6 +353,69 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ);
} }
@Test
public void shouldCreateValidationErrorForMissingRoleAndEmptyVerbs() throws Exception {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER);
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = MockHttpRequest
.create(HttpMethod.POST, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
.content("{ 'name' : 'permission_name', 'verbs' : [] }".replaceAll("'", "\"").getBytes())
.contentType(VndMediaType.REPOSITORY_PERMISSION);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentAsString()).contains("permission must either have a role or a not empty set of verbs");
}
@Test
public void shouldCreateValidationErrorForEmptyRoleAndEmptyVerbs() throws Exception {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER);
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = MockHttpRequest
.create(HttpMethod.POST, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
.content("{ 'name' : 'permission_name', 'role': '', 'verbs' : [] }".replaceAll("'", "\"").getBytes())
.contentType(VndMediaType.REPOSITORY_PERMISSION);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentAsString()).contains("permission must either have a role or a not empty set of verbs");
}
@Test
public void shouldCreateValidationErrorForRoleAndVerbs() throws Exception {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER);
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = MockHttpRequest
.create(HttpMethod.POST, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
.content("{ 'name' : 'permission_name', 'role': 'some role', 'verbs' : ['read'] }".replaceAll("'", "\"").getBytes())
.contentType(VndMediaType.REPOSITORY_PERMISSION);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentAsString()).contains("permission must either have a role or a not empty set of verbs");
}
@Test
public void shouldPassWithoutValidationErrorForRoleAndEmptyVerbs() throws Exception {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER);
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = MockHttpRequest
.create(HttpMethod.POST, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
.content("{ 'name' : 'permission_name', 'role': 'some role', 'verbs': [] }".replaceAll("'", "\"").getBytes())
.contentType(VndMediaType.REPOSITORY_PERMISSION);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(201);
}
@Test
public void shouldPassWithoutValidationErrorForRoleAndNoVerbs() throws Exception {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER);
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = MockHttpRequest
.create(HttpMethod.POST, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
.content("{ 'name' : 'permission_name', 'role': 'some role' }".replaceAll("'", "\"").getBytes())
.contentType(VndMediaType.REPOSITORY_PERMISSION);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(201);
}
private void assertGettingExpectedPermissions(ImmutableList<RepositoryPermission> expectedPermissions, String userPermission) throws URISyntaxException { private void assertGettingExpectedPermissions(ImmutableList<RepositoryPermission> expectedPermissions, String userPermission) throws URISyntaxException {
assertExpectedRequest(requestGETAllPermissions assertExpectedRequest(requestGETAllPermissions
.expectedResponseStatus(200) .expectedResponseStatus(200)