diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java index 4a458a6e6a..18e35b5aa7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -50,6 +50,7 @@ import java.util.LinkedHashSet; import java.util.Set; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableSet; //~--- JDK imports ------------------------------------------------------------ @@ -73,6 +74,7 @@ public class RepositoryPermission implements PermissionObject, Serializable private String name; @XmlElement(name = "verb") private Set verbs; + private String role; /** * This constructor exists for mapstruct and JAXB, only -- do not use this in "normal" code. @@ -87,6 +89,15 @@ public class RepositoryPermission implements PermissionObject, Serializable { this.name = name; 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; } @@ -116,8 +127,9 @@ public class RepositoryPermission implements PermissionObject, Serializable final RepositoryPermission other = (RepositoryPermission) obj; return Objects.equal(name, other.name) - && verbs.containsAll(other.verbs) && verbs.size() == other.verbs.size() + && verbs.containsAll(other.verbs) + && Objects.equal(role, other.role) && 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. // 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); } + /** + * 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. * @@ -192,7 +214,8 @@ public class RepositoryPermission implements PermissionObject, Serializable * @throws IllegalStateException when modified after the value has been set once. * * @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 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. * * @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 public void setName(String name) @@ -219,6 +243,22 @@ public class RepositoryPermission implements PermissionObject, Serializable 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. * @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) { throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); } - this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); + this.verbs = verbs == null? emptySet(): unmodifiableSet(new LinkedHashSet<>(verbs)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbs.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbs.java new file mode 100644 index 0000000000..d2f7fc6fd8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbs.java @@ -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[] payload() default {}; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbsValidator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbsValidator.java new file mode 100644 index 0000000000..194277ece3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbsValidator.java @@ -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 { + + 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; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java index fada89c44e..398e219207 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java @@ -14,6 +14,7 @@ import javax.validation.constraints.Pattern; import java.util.Collection; @Getter @Setter @ToString @NoArgsConstructor +@EitherRoleOrVerbs public class RepositoryPermissionDto extends HalRepresentation { public static final String GROUP_PREFIX = "@"; @@ -21,9 +22,11 @@ public class RepositoryPermissionDto extends HalRepresentation { @Pattern(regexp = ValidationUtil.REGEX_NAME) private String name; - @NotEmpty + @NoBlankStrings private Collection verbs; + private String role; + private boolean groupPermission = false; public RepositoryPermissionDto(String permissionName, boolean groupPermission) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index fb533ad280..e9ea0bead5 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -2,8 +2,6 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.databind.JsonNode; 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.inject.util.Providers; import de.otto.edison.hal.HalRepresentation; @@ -21,7 +19,6 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.spi.HttpRequest; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -35,6 +32,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermission; import sonia.scm.web.VndMediaType; +import javax.ws.rs.HttpMethod; import java.io.IOException; import java.io.UnsupportedEncodingException; 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; @Slf4j -@SubjectAware( - username = "trillian", - password = "secret", - configuration = "classpath:sonia/scm/repository/shiro.ini" -) public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAME = "repo"; @@ -114,9 +107,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { private Dispatcher dispatcher; - @Rule - public ShiroRule shiro = new ShiroRule(); - @Mock private RepositoryManager repositoryManager; @@ -363,6 +353,69 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { 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 expectedPermissions, String userPermission) throws URISyntaxException { assertExpectedRequest(requestGETAllPermissions .expectedResponseStatus(200)