From 9109efe2f1f67b892c97b879996023ed2642cc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 3 May 2019 07:59:18 +0200 Subject: [PATCH 01/91] Add role attribute to repository permission --- .../scm/repository/RepositoryPermission.java | 50 +++++++++++-- .../api/v2/resources/EitherRoleOrVerbs.java | 23 ++++++ .../resources/EitherRoleOrVerbsValidator.java | 30 ++++++++ .../v2/resources/RepositoryPermissionDto.java | 5 +- .../RepositoryPermissionRootResourceTest.java | 75 ++++++++++++++++--- 5 files changed, 166 insertions(+), 17 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbs.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/EitherRoleOrVerbsValidator.java 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) From 83b629222b42559f917a0ed37bdfae081178979a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 3 May 2019 13:26:18 +0200 Subject: [PATCH 02/91] Fix toString --- .../src/main/java/sonia/scm/repository/RepositoryPermission.java | 1 + 1 file changed, 1 insertion(+) 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 18e35b5aa7..6bd95c9f3d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -154,6 +154,7 @@ public class RepositoryPermission implements PermissionObject, Serializable //J- return MoreObjects.toStringHelper(this) .add("name", name) + .add("role", role) .add("verbs", verbs) .add("groupPermission", groupPermission) .toString(); From 232102716c982b6cff90e515ac05aed46d6c044c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 3 May 2019 14:19:33 +0200 Subject: [PATCH 03/91] Create DAO for repository roles --- .../sonia/scm/repository/RepositoryRole.java | 225 ++++++++++++++++++ .../scm/repository/RepositoryRoleDAO.java | 6 + .../repository/xml/XmlRepositoryRoleDAO.java | 33 +++ .../xml/XmlRepositoryRoleDatabase.java | 77 ++++++ .../repository/xml/XmlRepositoryRoleList.java | 74 ++++++ .../xml/XmlRepositoryRoleMapAdapter.java | 60 +++++ .../AvailableRepositoryPermissionsDto.java | 2 +- .../RepositoryPermissionResource.java | 6 +- .../sonia/scm/security/RepositoryRole.java | 43 ---- ...> SystemRepositoryPermissionProvider.java} | 10 +- ...stemRepositoryPermissionProviderTest.java} | 7 +- 11 files changed, 489 insertions(+), 54 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java create mode 100644 scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java create mode 100644 scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java create mode 100644 scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleList.java create mode 100644 scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleMapAdapter.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java rename scm-webapp/src/main/java/sonia/scm/security/{RepositoryPermissionProvider.java => SystemRepositoryPermissionProvider.java} (90%) rename scm-webapp/src/test/java/sonia/scm/security/{RepositoryPermissionProviderTest.java => SystemRepositoryPermissionProviderTest.java} (89%) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java new file mode 100644 index 0000000000..ca77f255f8 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java @@ -0,0 +1,225 @@ +/* + Copyright (c) 2010, Sebastian Sdorra + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of SCM-Manager; nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + + +package sonia.scm.repository; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import sonia.scm.ModelObject; +import sonia.scm.user.User; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; +import java.util.Collections; +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; + +/** + * Custom role with specific permissions related to {@link Repository}. + * This object should be immutable, but could not be due to mapstruct. + */ +@XmlRootElement(name = "roles") +@XmlAccessorType(XmlAccessType.FIELD) +public class RepositoryRole implements ModelObject { + + private static final long serialVersionUID = -723588336073192740L; + + private static final String REPOSITORY_MODIFIED_EXCEPTION_TEXT = "roles must not be modified"; + + private String name; + @XmlElement(name = "verb") + private Set verbs; + + private Long creationDate; + private Long lastModified; + private String type; + + /** + * This constructor exists for mapstruct and JAXB, only -- do not use this in "normal" code. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryRole#RepositoryRole(String, Collection, String)} instead. + */ + @Deprecated + public RepositoryRole() {} + + public RepositoryRole(String name, Collection verbs, String type) { + this.name = name; + this.verbs = new LinkedHashSet<>(verbs); + this.type = type; + } + + /** + * Returns true if the {@link RepositoryRole} is the same as the obj argument. + * + * + * @param obj the reference object with which to compare + * + * @return true if the {@link RepositoryRole} is the same as the obj argument + */ + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + final RepositoryRole other = (RepositoryRole) obj; + + return Objects.equal(name, other.name) + && verbs.size() == other.verbs.size() + && verbs.containsAll(other.verbs); + } + + /** + * Returns the hash code value for the {@link RepositoryRole}. + * + * + * @return the hash code value for the {@link RepositoryRole} + */ + @Override + public int hashCode() { + return Objects.hashCode(name, verbs); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("verbs", verbs) + .toString(); + } + + public String getName() { + return name; + } + + /** + * Returns the verb of the role. + */ + public Collection getVerbs() { + return verbs == null ? emptyList() : Collections.unmodifiableSet(verbs); + } + + @Override + public String getId() { + return name; + } + + @Override + public void setLastModified(Long timestamp) { + this.lastModified = timestamp; + } + + @Override + public Long getCreationDate() { + return creationDate; + } + + @Override + public void setCreationDate(Long timestamp) { + this.creationDate = timestamp; + } + + @Override + public Long getLastModified() { + return lastModified; + } + + @Override + public String getType() { + return type; + } + + public void setType(String type) { + if (this.type != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.type = type; + } + + @Override + public boolean isValid() { + return !Strings.isNullOrEmpty(name) && !verbs.isEmpty(); + } + + /** + * 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 RepositoryRole#RepositoryRole(String, Collection, String)} instead. + */ + @Deprecated + public void setName(String name) { + if (this.name != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + 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 RepositoryRole#RepositoryRole(String, Collection, String)} instead. + */ + @Deprecated + public void setVerbs(Collection verbs) { + if (this.verbs != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.verbs = verbs == null? emptySet(): unmodifiableSet(new LinkedHashSet<>(verbs)); + } + + @Override + public RepositoryRole clone() { + try { + return (RepositoryRole) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java new file mode 100644 index 0000000000..82bd163989 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java @@ -0,0 +1,6 @@ +package sonia.scm.repository; + +import sonia.scm.GenericDAO; + +public interface RepositoryRoleDAO extends GenericDAO { +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java new file mode 100644 index 0000000000..4fa814e3c8 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java @@ -0,0 +1,33 @@ +package sonia.scm.repository.xml; + +import com.google.inject.Inject; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleDAO; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.xml.AbstractXmlDAO; + +public class XmlRepositoryRoleDAO extends AbstractXmlDAO + implements RepositoryRoleDAO { + + public static final String STORE_NAME = "repositoryRoles"; + + @Inject + public XmlRepositoryRoleDAO(ConfigurationStoreFactory storeFactory) { + super(storeFactory + .withType(XmlRepositoryRoleDatabase.class) + .withName(STORE_NAME) + .build()); + } + + @Override + protected RepositoryRole clone(RepositoryRole role) + { + return role.clone(); + } + + @Override + protected XmlRepositoryRoleDatabase createNewDatabase() + { + return new XmlRepositoryRoleDatabase(); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java new file mode 100644 index 0000000000..8219d32a67 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java @@ -0,0 +1,77 @@ +package sonia.scm.repository.xml; + +import sonia.scm.repository.RepositoryRole; +import sonia.scm.xml.XmlDatabase; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +@XmlRootElement(name = "user-db") +@XmlAccessorType(XmlAccessType.FIELD) +public class XmlRepositoryRoleDatabase implements XmlDatabase { + + private Long creationTime; + private Long lastModified; + + @XmlJavaTypeAdapter(XmlRepositoryRoleMapAdapter.class) + @XmlElement(name = "roles") + private Map roleMap = new LinkedHashMap(); + + public XmlRepositoryRoleDatabase() { + long c = System.currentTimeMillis(); + + creationTime = c; + lastModified = c; + } + + @Override + public void add(RepositoryRole role) { + roleMap.put(role.getName(), role); + } + + @Override + public boolean contains(String name) { + return roleMap.containsKey(name); + } + + @Override + public RepositoryRole remove(String name) { + return roleMap.remove(name); + } + + @Override + public Collection values() { + return roleMap.values(); + } + + @Override + public RepositoryRole get(String name) { + return roleMap.get(name); + } + + @Override + public long getCreationTime() { + return creationTime; + } + + @Override + public long getLastModified() { + return lastModified; + } + + @Override + public void setCreationTime(long creationTime) { + this.creationTime = creationTime; + } + + @Override + public void setLastModified(long lastModified) { + this.lastModified = lastModified; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleList.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleList.java new file mode 100644 index 0000000000..7910d4300a --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleList.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository.xml; + +import sonia.scm.repository.RepositoryRole; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +@XmlRootElement(name = "roles") +@XmlAccessorType(XmlAccessType.FIELD) +public class XmlRepositoryRoleList implements Iterable { + + public XmlRepositoryRoleList() {} + + public XmlRepositoryRoleList(Map roleMap) { + this.roles = new LinkedList(roleMap.values()); + } + + @Override + public Iterator iterator() + { + return roles.iterator(); + } + + public LinkedList getRoles() + { + return roles; + } + + public void setRoles(LinkedList roles) + { + this.roles = roles; + } + + @XmlElement(name = "role") + private LinkedList roles; +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleMapAdapter.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleMapAdapter.java new file mode 100644 index 0000000000..959eff331a --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleMapAdapter.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository.xml; + +import sonia.scm.repository.RepositoryRole; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.util.LinkedHashMap; +import java.util.Map; + +public class XmlRepositoryRoleMapAdapter + extends XmlAdapter> { + + @Override + public XmlRepositoryRoleList marshal(Map roleMap) { + return new XmlRepositoryRoleList(roleMap); + } + + @Override + public Map unmarshal(XmlRepositoryRoleList roles) { + Map roleMap = new LinkedHashMap<>(); + + for (RepositoryRole role : roles) { + roleMap.put(role.getName(), role); + } + + return roleMap; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java index 60203b565b..681be90a38 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java @@ -2,7 +2,7 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; -import sonia.scm.security.RepositoryRole; +import sonia.scm.repository.RepositoryRole; import java.util.Collection; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java index e5734085ca..df042f3e61 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import de.otto.edison.hal.Links; -import sonia.scm.security.RepositoryPermissionProvider; +import sonia.scm.security.SystemRepositoryPermissionProvider; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -19,11 +19,11 @@ public class RepositoryPermissionResource { static final String PATH = "v2/repositoryPermissions/"; - private final RepositoryPermissionProvider repositoryPermissionProvider; + private final SystemRepositoryPermissionProvider repositoryPermissionProvider; private final ResourceLinks resourceLinks; @Inject - public RepositoryPermissionResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { + public RepositoryPermissionResource(SystemRepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { this.repositoryPermissionProvider = repositoryPermissionProvider; this.resourceLinks = resourceLinks; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java b/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java deleted file mode 100644 index 1fab500d79..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java +++ /dev/null @@ -1,43 +0,0 @@ -package sonia.scm.security; - -import java.util.Collection; -import java.util.Collections; -import java.util.Objects; - -public class RepositoryRole { - - private final String name; - private final Collection verbs; - - public RepositoryRole(String name, Collection verbs) { - this.name = name; - this.verbs = verbs; - } - - public String getName() { - return name; - } - - public Collection getVerbs() { - return Collections.unmodifiableCollection(verbs); - } - - public String toString() { - return "Role " + name + " (" + String.join(", ", verbs) + ")"; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof RepositoryRole)) return false; - RepositoryRole that = (RepositoryRole) o; - return name.equals(that.name) - && this.verbs.containsAll(that.verbs) - && this.verbs.size() == that.verbs.size(); - } - - @Override - public int hashCode() { - return Objects.hash(name, verbs.size()); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java similarity index 90% rename from scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java rename to scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java index 070990c6d6..5856abbc8b 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java @@ -4,6 +4,7 @@ import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.RepositoryRole; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; @@ -23,19 +24,20 @@ import java.util.Set; import java.util.stream.Collectors; import static java.util.Collections.unmodifiableCollection; +import static java.util.Collections.unmodifiableList; -public class RepositoryPermissionProvider { +public class SystemRepositoryPermissionProvider { - private static final Logger logger = LoggerFactory.getLogger(RepositoryPermissionProvider.class); + private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class); private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml"; private final Collection availableVerbs; private final Collection availableRoles; @Inject - public RepositoryPermissionProvider(PluginLoader pluginLoader) { + public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) { AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader); this.availableVerbs = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableVerbs)); - this.availableRoles = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs)).collect(Collectors.toList()))); + this.availableRoles = unmodifiableList(new LinkedHashSet<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs, "system")).collect(Collectors.toList()))); } public Collection availableVerbs() { diff --git a/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java b/scm-webapp/src/test/java/sonia/scm/security/SystemRepositoryPermissionProviderTest.java similarity index 89% rename from scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java rename to scm-webapp/src/test/java/sonia/scm/security/SystemRepositoryPermissionProviderTest.java index 8a8d85fdb2..8d1685b926 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/SystemRepositoryPermissionProviderTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.RepositoryRole; import sonia.scm.util.ClassLoaders; import java.lang.reflect.Field; @@ -15,9 +16,9 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -class RepositoryPermissionProviderTest { +class SystemRepositoryPermissionProviderTest { - private RepositoryPermissionProvider repositoryPermissionProvider; + private SystemRepositoryPermissionProvider repositoryPermissionProvider; private String[] allVerbsFromRepositoryClass; @@ -25,7 +26,7 @@ class RepositoryPermissionProviderTest { void init() { PluginLoader pluginLoader = mock(PluginLoader.class); when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); - repositoryPermissionProvider = new RepositoryPermissionProvider(pluginLoader); + repositoryPermissionProvider = new SystemRepositoryPermissionProvider(pluginLoader); allVerbsFromRepositoryClass = Arrays.stream(RepositoryPermissions.class.getDeclaredFields()) .filter(field -> field.getName().startsWith("ACTION_")) .filter(field -> !field.getName().equals("ACTION_HEALTHCHECK")) From c0760688e9f954216ab8679db947d0dc3e4b6835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 3 May 2019 14:47:49 +0200 Subject: [PATCH 04/91] Join repository roles from the system and from the database --- .../scm/repository/RepositoryRoleDAO.java | 4 ++ .../repository/xml/XmlRepositoryRoleDAO.java | 7 +++ .../RepositoryPermissionResource.java | 6 +- .../RepositoryPermissionProvider.java | 60 +++++++++++++++++++ .../SystemRepositoryPermissionProvider.java | 14 ++--- .../RepositoryPermissionProviderTest.java | 51 ++++++++++++++++ 6 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java create mode 100644 scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java index 82bd163989..3d7e53b3a2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java @@ -2,5 +2,9 @@ package sonia.scm.repository; import sonia.scm.GenericDAO; +import java.util.List; + public interface RepositoryRoleDAO extends GenericDAO { + @Override + List getAll(); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java index 4fa814e3c8..c913d57ee4 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java @@ -6,6 +6,8 @@ import sonia.scm.repository.RepositoryRoleDAO; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.xml.AbstractXmlDAO; +import java.util.List; + public class XmlRepositoryRoleDAO extends AbstractXmlDAO implements RepositoryRoleDAO { @@ -30,4 +32,9 @@ public class XmlRepositoryRoleDAO extends AbstractXmlDAO getAll() { + return (List) super.getAll(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java index df042f3e61..e5734085ca 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import de.otto.edison.hal.Links; -import sonia.scm.security.SystemRepositoryPermissionProvider; +import sonia.scm.security.RepositoryPermissionProvider; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -19,11 +19,11 @@ public class RepositoryPermissionResource { static final String PATH = "v2/repositoryPermissions/"; - private final SystemRepositoryPermissionProvider repositoryPermissionProvider; + private final RepositoryPermissionProvider repositoryPermissionProvider; private final ResourceLinks resourceLinks; @Inject - public RepositoryPermissionResource(SystemRepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { + public RepositoryPermissionResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { this.repositoryPermissionProvider = repositoryPermissionProvider; this.resourceLinks = resourceLinks; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java new file mode 100644 index 0000000000..292feee7c1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java @@ -0,0 +1,60 @@ +package sonia.scm.security; + +import com.google.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleDAO; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.IOException; +import java.net.URL; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static java.util.Collections.unmodifiableCollection; + +public class RepositoryPermissionProvider { + + private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider; + private final RepositoryRoleDAO repositoryRoleDAO; + + @Inject + public RepositoryPermissionProvider(SystemRepositoryPermissionProvider systemRepositoryPermissionProvider, RepositoryRoleDAO repositoryRoleDAO) { + this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider; + this.repositoryRoleDAO = repositoryRoleDAO; + } + + public Collection availableVerbs() { + return systemRepositoryPermissionProvider.availableVerbs(); + } + + public Collection availableRoles() { + List customRoles = repositoryRoleDAO.getAll(); + List availableSystemRoles = systemRepositoryPermissionProvider.availableRoles(); + + return new AbstractList() { + @Override + public RepositoryRole get(int index) { + return index < availableSystemRoles.size()? availableSystemRoles.get(index): customRoles.get(index - availableSystemRoles.size()); + } + + @Override + public int size() { + return availableSystemRoles.size() + customRoles.size(); + } + }; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java index 5856abbc8b..63fdeec702 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java @@ -26,25 +26,25 @@ import java.util.stream.Collectors; import static java.util.Collections.unmodifiableCollection; import static java.util.Collections.unmodifiableList; -public class SystemRepositoryPermissionProvider { +class SystemRepositoryPermissionProvider { private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class); private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml"; - private final Collection availableVerbs; - private final Collection availableRoles; + private final List availableVerbs; + private final List availableRoles; @Inject public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) { AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader); - this.availableVerbs = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableVerbs)); - this.availableRoles = unmodifiableList(new LinkedHashSet<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs, "system")).collect(Collectors.toList()))); + this.availableVerbs = unmodifiableList(new ArrayList<>(availablePermissions.availableVerbs)); + this.availableRoles = unmodifiableList(new ArrayList<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs, "system")).collect(Collectors.toList()))); } - public Collection availableVerbs() { + public List availableVerbs() { return availableVerbs; } - public Collection availableRoles() { + public List availableRoles() { return availableRoles; } diff --git a/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java b/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java new file mode 100644 index 0000000000..30e264dc6c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java @@ -0,0 +1,51 @@ +package sonia.scm.security; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleDAO; + +import java.util.Collection; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryPermissionProviderTest { + + @Mock + SystemRepositoryPermissionProvider systemRepositoryPermissionProvider; + @Mock + RepositoryRoleDAO repositoryRoleDAO; + + @InjectMocks + RepositoryPermissionProvider repositoryPermissionProvider; + + @Test + void shouldReturnVerbsFromSystem() { + List expectedVerbs = asList("verb1", "verb2"); + when(systemRepositoryPermissionProvider.availableVerbs()).thenReturn(expectedVerbs); + + Collection actualVerbs = repositoryPermissionProvider.availableVerbs(); + + assertThat(actualVerbs).isEqualTo(expectedVerbs); + } + + @Test + void shouldReturnJoinedRolesFromSystemAndDao() { + RepositoryRole systemRole = new RepositoryRole("roleSystem", singletonList("verb1"), "system"); + RepositoryRole daoRole = new RepositoryRole("roleDao", singletonList("verb1"), "xml"); + when(systemRepositoryPermissionProvider.availableRoles()).thenReturn(singletonList(systemRole)); + when(repositoryRoleDAO.getAll()).thenReturn(singletonList(daoRole)); + + Collection actualRoles = repositoryPermissionProvider.availableRoles(); + + assertThat(actualRoles).containsExactly(systemRole, daoRole); + } +} From 7a2166a3658984ba94448615adca76b3717bc829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 3 May 2019 16:30:50 +0200 Subject: [PATCH 05/91] Bind repository role dao --- scm-webapp/src/main/java/sonia/scm/ScmServletModule.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index a3661a25f0..898baf33e1 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -75,10 +75,12 @@ import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManagerProvider; import sonia.scm.repository.RepositoryProvider; +import sonia.scm.repository.RepositoryRoleDAO; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.xml.XmlRepositoryDAO; +import sonia.scm.repository.xml.XmlRepositoryRoleDAO; import sonia.scm.schedule.QuartzScheduler; import sonia.scm.schedule.Scheduler; import sonia.scm.security.AccessTokenCookieIssuer; @@ -267,6 +269,7 @@ public class ScmServletModule extends ServletModule bind(GroupDAO.class, XmlGroupDAO.class); bind(UserDAO.class, XmlUserDAO.class); bind(RepositoryDAO.class, XmlRepositoryDAO.class); + bind(RepositoryRoleDAO.class, XmlRepositoryRoleDAO.class); bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class, RepositoryManagerProvider.class); From 76b9100e7e687444f631d4af05881f716c625aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 3 May 2019 17:44:27 +0200 Subject: [PATCH 06/91] Add endpoint for repository roles (tests pending) --- .../AbstractRepositoryRoleManager.java | 78 +++++++ .../sonia/scm/repository/RepositoryRole.java | 6 +- .../scm/repository/RepositoryRoleEvent.java | 69 +++++++ .../scm/repository/RepositoryRoleManager.java | 44 ++++ .../RepositoryRoleModificationEvent.java | 67 ++++++ .../main/java/sonia/scm/web/VndMediaType.java | 3 + .../main/java/sonia/scm/ScmServletModule.java | 3 + .../scm/api/v2/resources/MapperModule.java | 4 + .../RepositoryRoleCollectionResource.java | 95 +++++++++ .../RepositoryRoleCollectionToDtoMapper.java | 34 ++++ .../api/v2/resources/RepositoryRoleDto.java | 22 ++ ...positoryRoleDtoToRepositoryRoleMapper.java | 14 ++ .../v2/resources/RepositoryRoleResource.java | 103 ++++++++++ .../resources/RepositoryRoleRootResource.java | 34 ++++ ...positoryRoleToRepositoryRoleDtoMapper.java | 42 ++++ .../scm/api/v2/resources/ResourceLinks.java | 43 +++- .../DefaultRepositoryRoleManager.java | 191 ++++++++++++++++++ 17 files changed, 849 insertions(+), 3 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryRoleManager.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryRoleEvent.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryRoleManager.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryRoleModificationEvent.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionToDtoMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDtoToRepositoryRoleMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryRoleManager.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryRoleManager.java new file mode 100644 index 0000000000..1db0065e8b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryRoleManager.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository; + +import sonia.scm.HandlerEventType; +import sonia.scm.event.ScmEventBus; + +/** + * Abstract base class for {@link RepositoryRoleManager} implementations. This class + * implements the listener methods of the {@link RepositoryRoleManager} interface. + */ +public abstract class AbstractRepositoryRoleManager implements RepositoryRoleManager { + + /** + * Send a {@link RepositoryRoleEvent} to the {@link ScmEventBus}. + * + * @param event type of change event + * @param repositoryRole repositoryRole that has changed + * @param oldRepositoryRole old repositoryRole + */ + protected void fireEvent(HandlerEventType event, RepositoryRole repositoryRole, RepositoryRole oldRepositoryRole) + { + fireEvent(new RepositoryRoleModificationEvent(event, repositoryRole, oldRepositoryRole)); + } + + /** + * Creates a new {@link RepositoryRoleEvent} and calls {@link #fireEvent(RepositoryRoleEvent)}. + * + * @param repositoryRole repositoryRole that has changed + * @param event type of change event + */ + protected void fireEvent(HandlerEventType event, RepositoryRole repositoryRole) + { + fireEvent(new RepositoryRoleEvent(event, repositoryRole)); + } + + /** + * Send a {@link RepositoryRoleEvent} to the {@link ScmEventBus}. + * + * @param event repositoryRole event + * @since 1.48 + */ + protected void fireEvent(RepositoryRoleEvent event) + { + ScmEventBus.getInstance().post(event); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java index ca77f255f8..42ee8ffa65 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java @@ -33,11 +33,12 @@ package sonia.scm.repository; +import com.github.sdorra.ssp.PermissionObject; +import com.github.sdorra.ssp.StaticPermissions; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Strings; import sonia.scm.ModelObject; -import sonia.scm.user.User; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -56,9 +57,10 @@ import static java.util.Collections.unmodifiableSet; * Custom role with specific permissions related to {@link Repository}. * This object should be immutable, but could not be due to mapstruct. */ +@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"read", "modify"}) @XmlRootElement(name = "roles") @XmlAccessorType(XmlAccessType.FIELD) -public class RepositoryRole implements ModelObject { +public class RepositoryRole implements ModelObject, PermissionObject { private static final long serialVersionUID = -723588336073192740L; diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleEvent.java new file mode 100644 index 0000000000..fcd21bfbcd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleEvent.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository; + +import sonia.scm.HandlerEventType; +import sonia.scm.event.AbstractHandlerEvent; +import sonia.scm.event.Event; + +/** + * The RepositoryRoleEvent is fired if a repository role object changes. + * @since 2.0 + */ +@Event +public class RepositoryRoleEvent extends AbstractHandlerEvent { + + /** + * Constructs a new repositoryRole event. + * + * + * @param eventType event type + * @param repositoryRole changed repositoryRole + */ + public RepositoryRoleEvent(HandlerEventType eventType, RepositoryRole repositoryRole) { + super(eventType, repositoryRole); + } + + /** + * Constructs a new repositoryRole event. + * + * + * @param eventType type of the event + * @param repositoryRole changed repositoryRole + * @param oldRepositoryRole old repositoryRole + */ + public RepositoryRoleEvent(HandlerEventType eventType, RepositoryRole repositoryRole, RepositoryRole oldRepositoryRole) { + super(eventType, repositoryRole, oldRepositoryRole); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleManager.java new file mode 100644 index 0000000000..c7e1971110 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleManager.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository; + +import sonia.scm.Manager; +import sonia.scm.search.Searchable; + +/** + * The central class for managing {@link RepositoryRole} objects. + * This class is a singleton and is available via injection. + */ +public interface RepositoryRoleManager extends Manager { +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleModificationEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleModificationEvent.java new file mode 100644 index 0000000000..eabeb26b2e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleModificationEvent.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository; + +import sonia.scm.HandlerEventType; +import sonia.scm.ModificationHandlerEvent; +import sonia.scm.event.Event; + +/** + * Event which is fired whenever a repository role is modified. + * + * @since 2.0 + */ +@Event +public class RepositoryRoleModificationEvent extends RepositoryRoleEvent implements ModificationHandlerEvent +{ + + private final RepositoryRole itemBeforeModification; + + /** + * Constructs a new {@link RepositoryRoleModificationEvent}. + * + * @param eventType type of event + * @param item changed repository role + * @param itemBeforeModification changed repository role before it was modified + */ + public RepositoryRoleModificationEvent(HandlerEventType eventType, RepositoryRole item, RepositoryRole itemBeforeModification) + { + super(eventType, item); + this.itemBeforeModification = itemBeforeModification; + } + + @Override + public RepositoryRole getItemBeforeModification() + { + return itemBeforeModification; + } + +} diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 4dcb2f1d96..cad62821ed 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -53,6 +53,9 @@ public class VndMediaType { public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX; + public static final String REPOSITORY_ROLE = PREFIX + "repositoryRole" + SUFFIX; + public static final String REPOSITORY_ROLE_COLLECTION = PREFIX + "repositoryRoleCollection" + SUFFIX; + private VndMediaType() { } diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 898baf33e1..8cc34b6f23 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -67,6 +67,7 @@ import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginManager; import sonia.scm.repository.DefaultRepositoryManager; import sonia.scm.repository.DefaultRepositoryProvider; +import sonia.scm.repository.DefaultRepositoryRoleManager; import sonia.scm.repository.HealthCheckContextListener; import sonia.scm.repository.NamespaceStrategy; import sonia.scm.repository.NamespaceStrategyProvider; @@ -76,6 +77,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManagerProvider; import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRoleDAO; +import sonia.scm.repository.RepositoryRoleManager; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HookEventFacade; @@ -270,6 +272,7 @@ public class ScmServletModule extends ServletModule bind(UserDAO.class, XmlUserDAO.class); bind(RepositoryDAO.class, XmlRepositoryDAO.class); bind(RepositoryRoleDAO.class, XmlRepositoryRoleDAO.class); + bind(RepositoryRoleManager.class).to(DefaultRepositoryRoleManager.class); bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class, RepositoryManagerProvider.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index c74f16ad70..cf09eeb128 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -28,6 +28,10 @@ public class MapperModule extends AbstractModule { bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass()); bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass()); + bind(RepositoryRoleToRepositoryRoleDtoMapper.class).to(Mappers.getMapper(RepositoryRoleToRepositoryRoleDtoMapper.class).getClass()); + bind(RepositoryRoleDtoToRepositoryRoleMapper.class).to(Mappers.getMapper(RepositoryRoleDtoToRepositoryRoleMapper.class).getClass()); + bind(RepositoryRoleCollectionToDtoMapper.class); + bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(DefaultChangesetToChangesetDtoMapper.class).getClass()); bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java new file mode 100644 index 0000000000..12f52156f0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java @@ -0,0 +1,95 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import org.apache.shiro.authc.credential.PasswordService; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +public class RepositoryRoleCollectionResource { + + private static final int DEFAULT_PAGE_SIZE = 10; + private final RepositoryRoleDtoToRepositoryRoleMapper dtoToRepositoryRoleMapper; + private final RepositoryRoleCollectionToDtoMapper repositoryRoleCollectionToDtoMapper; + private final ResourceLinks resourceLinks; + + private final IdResourceManagerAdapter adapter; + + @Inject + public RepositoryRoleCollectionResource(RepositoryRoleManager manager, RepositoryRoleDtoToRepositoryRoleMapper dtoToRepositoryRoleMapper, + RepositoryRoleCollectionToDtoMapper repositoryRoleCollectionToDtoMapper, ResourceLinks resourceLinks) { + this.dtoToRepositoryRoleMapper = dtoToRepositoryRoleMapper; + this.repositoryRoleCollectionToDtoMapper = repositoryRoleCollectionToDtoMapper; + this.adapter = new IdResourceManagerAdapter<>(manager, RepositoryRole.class); + this.resourceLinks = resourceLinks; + } + + /** + * Returns all repository roles for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). + * + * Note: This method requires "repositoryRole" privilege. + * + * @param page the number of the requested page + * @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE}) + * @param sortBy sort parameter (if empty - undefined sorting) + * @param desc sort direction desc or asc + */ + @GET + @Path("") + @Produces(VndMediaType.REPOSITORY_ROLE_COLLECTION) + @TypeHint(CollectionDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "\"sortBy\" field unknown"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current repositoryRole does not have the \"repositoryRole\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc + ) { + return adapter.getAll(page, pageSize, x -> true, sortBy, desc, + pageResult -> repositoryRoleCollectionToDtoMapper.map(page, pageSize, pageResult)); + } + + /** + * Creates a new repository role. + * + * Note: This method requires "repositoryRole" privilege. + * + * @param repositoryRole The repositoryRole to be created. + * @return A response with the link to the new repository role (if created successfully). + */ + @POST + @Path("") + @Consumes(VndMediaType.REPOSITORY_ROLE) + @StatusCodes({ + @ResponseCode(code = 201, condition = "create success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"), + @ResponseCode(code = 409, condition = "conflict, a repository role with this name already exists"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repositoryRole")) + public Response create(@Valid RepositoryRoleDto repositoryRole) { + return adapter.create(repositoryRole, () -> dtoToRepositoryRoleMapper.map(repositoryRole), u -> resourceLinks.repositoryRole().self(u.getName())); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionToDtoMapper.java new file mode 100644 index 0000000000..ab34efd7fe --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionToDtoMapper.java @@ -0,0 +1,34 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRolePermissions; + +import javax.inject.Inject; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +public class RepositoryRoleCollectionToDtoMapper extends BasicCollectionToDtoMapper { + + private final ResourceLinks resourceLinks; + + @Inject + public RepositoryRoleCollectionToDtoMapper(RepositoryRoleToRepositoryRoleDtoMapper repositoryRoleToDtoMapper, ResourceLinks resourceLinks) { + super("repositoryRoles", repositoryRoleToDtoMapper); + this.resourceLinks = resourceLinks; + } + + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult) { + return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink()); + } + + Optional createCreateLink() { + return RepositoryRolePermissions.modify().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty(); + } + + String createSelfLink() { + return resourceLinks.repositoryRoleCollection().self(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java new file mode 100644 index 0000000000..9bd64b4ce8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java @@ -0,0 +1,22 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Collection; + +@Getter +@Setter +@NoArgsConstructor +public class RepositoryRoleDto extends HalRepresentation { + private String name; + private Collection verbs; + + RepositoryRoleDto(Links links, Embedded embedded) { + super(links, embedded); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDtoToRepositoryRoleMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDtoToRepositoryRoleMapper.java new file mode 100644 index 0000000000..dc969b59b3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDtoToRepositoryRoleMapper.java @@ -0,0 +1,14 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import sonia.scm.repository.RepositoryRole; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class RepositoryRoleDtoToRepositoryRoleMapper extends BaseDtoMapper { + + @Mapping(target = "creationDate", ignore = true) + public abstract RepositoryRole map(RepositoryRoleDto repositoryRoleDto); +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java new file mode 100644 index 0000000000..59adbce264 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleResource.java @@ -0,0 +1,103 @@ +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 sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +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; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +public class RepositoryRoleResource { + + private final RepositoryRoleDtoToRepositoryRoleMapper dtoToRepositoryRoleMapper; + private final RepositoryRoleToRepositoryRoleDtoMapper repositoryRoleToDtoMapper; + + private final IdResourceManagerAdapter adapter; + + @Inject + public RepositoryRoleResource( + RepositoryRoleDtoToRepositoryRoleMapper dtoToRepositoryRoleMapper, + RepositoryRoleToRepositoryRoleDtoMapper repositoryRoleToDtoMapper, + RepositoryRoleManager manager) { + this.dtoToRepositoryRoleMapper = dtoToRepositoryRoleMapper; + this.repositoryRoleToDtoMapper = repositoryRoleToDtoMapper; + this.adapter = new IdResourceManagerAdapter<>(manager, RepositoryRole.class); + } + + /** + * Returns a repository role. + * + * Note: This method requires "repositoryRole" privilege. + * + * @param name the id/name of the repository role + */ + @GET + @Path("") + @Produces(VndMediaType.REPOSITORY_ROLE) + @TypeHint(RepositoryRoleDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository role"), + @ResponseCode(code = 404, condition = "not found, no repository role with the specified name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get(@PathParam("name") String name) { + return adapter.get(name, repositoryRoleToDtoMapper::map); + } + + /** + * Deletes a repository role. + * + * Note: This method requires "repositoryRole" privilege. + * + * @param name the name of the repository role to delete. + */ + @DELETE + @Path("") + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response delete(@PathParam("name") String name) { + return adapter.delete(name); + } + + /** + * Modifies the given repository role. + * + * Note: This method requires "repositoryRole" privilege. + * + * @param name name of the repository role to be modified + * @param repositoryRole repository role object to modify + */ + @PUT + @Path("") + @Consumes(VndMediaType.REPOSITORY_ROLE) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of repository role name"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"), + @ResponseCode(code = 404, condition = "not found, no repository role with the specified name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(@PathParam("name") String name, @Valid RepositoryRoleDto repositoryRole) { + return adapter.update(name, existing -> dtoToRepositoryRoleMapper.map(repositoryRole)); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java new file mode 100644 index 0000000000..f4adb02438 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java @@ -0,0 +1,34 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +/** + * RESTful web service resource to manage repository roles. + */ +@Path(RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2) +public class RepositoryRoleRootResource { + + static final String REPOSITORY_ROLES_PATH_V2 = "v2/repository-roles/"; + + private final Provider repositoryRoleCollectionResource; + private final Provider repositoryRoleResource; + + @Inject + public RepositoryRoleRootResource(Provider repositoryRoleCollectionResource, + Provider repositoryRoleResource) { + this.repositoryRoleCollectionResource = repositoryRoleCollectionResource; + this.repositoryRoleResource = repositoryRoleResource; + } + + @Path("") + public RepositoryRoleCollectionResource getRepositoryRoleCollectionResource() { + return repositoryRoleCollectionResource.get(); + } + + @Path("{name}") + public RepositoryRoleResource getRepositoryRoleResource() { + return repositoryRoleResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java new file mode 100644 index 0000000000..86b4709fd2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java @@ -0,0 +1,42 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.Links; +import org.mapstruct.Mapper; +import org.mapstruct.ObjectFactory; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.repository.RepositoryRolePermissions; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper { + + @Inject + private RepositoryRoleManager repositoryRoleManager; + + @Inject + private ResourceLinks resourceLinks; + + @ObjectFactory + RepositoryRoleDto createDto(RepositoryRole repositoryRole) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName())); + if (RepositoryRolePermissions.modify().isPermitted()) { + linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName()))); + linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName()))); + } + + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), repositoryRole); + + return new RepositoryRoleDto(linksBuilder.build(), embeddedBuilder.build()); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index ff1013bb76..141645a09c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -172,7 +172,6 @@ class ResourceLinks { } } - UserCollectionLinks userCollection() { return new UserCollectionLinks(scmPathInfoStore.get()); } @@ -522,8 +521,50 @@ class ResourceLinks { public String content(String namespace, String name, String revision, String path) { return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, "").href(), path); } + } + RepositoryRoleLinks repositoryRole() { + return new RepositoryRoleLinks(scmPathInfoStore.get()); + } + static class RepositoryRoleLinks { + private final LinkBuilder repositoryRoleLinkBuilder; + + RepositoryRoleLinks(ScmPathInfo pathInfo) { + repositoryRoleLinkBuilder = new LinkBuilder(pathInfo, RepositoryRoleRootResource.class, RepositoryRoleResource.class); + } + + String self(String name) { + return repositoryRoleLinkBuilder.method("getRepositoryRoleResource").parameters(name).method("get").parameters().href(); + } + + String delete(String name) { + return repositoryRoleLinkBuilder.method("getRepositoryRoleResource").parameters(name).method("delete").parameters().href(); + } + + String update(String name) { + return repositoryRoleLinkBuilder.method("getRepositoryRoleResource").parameters(name).method("update").parameters().href(); + } + } + + RepositoryRoleCollectionLinks repositoryRoleCollection() { + return new RepositoryRoleCollectionLinks(scmPathInfoStore.get()); + } + + static class RepositoryRoleCollectionLinks { + private final LinkBuilder collectionLinkBuilder; + + RepositoryRoleCollectionLinks(ScmPathInfo pathInfo) { + collectionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRoleRootResource.class, RepositoryRoleCollectionResource.class); + } + + String self() { + return collectionLinkBuilder.method("getRepositoryRoleCollectionResource").parameters().method("getAll").parameters().href(); + } + + String create() { + return collectionLinkBuilder.method("getRepositoryRoleCollectionResource").parameters().method("create").parameters().href(); + } } public RepositoryPermissionLinks repositoryPermission() { diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java new file mode 100644 index 0000000000..bc52d38dd2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository; + +import com.github.sdorra.ssp.PermissionActionCheck; +import com.github.sdorra.ssp.PermissionCheck; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.EagerSingleton; +import sonia.scm.HandlerEventType; +import sonia.scm.ManagerDaoAdapter; +import sonia.scm.NotFoundException; +import sonia.scm.SCMContextProvider; +import sonia.scm.util.Util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +@Singleton @EagerSingleton +public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager +{ + + /** the logger for XmlRepositoryRoleManager */ + private static final Logger logger = + LoggerFactory.getLogger(DefaultRepositoryRoleManager.class); + + @Inject + public DefaultRepositoryRoleManager(RepositoryRoleDAO repositoryRoleDAO) + { + this.repositoryRoleDAO = repositoryRoleDAO; + this.managerDaoAdapter = new ManagerDaoAdapter<>(repositoryRoleDAO); + } + + @Override + public void close() { + // do nothing + } + + @Override + public RepositoryRole create(RepositoryRole repositoryRole) { + String type = repositoryRole.getType(); + if (Util.isEmpty(type)) { + repositoryRole.setType(repositoryRoleDAO.getType()); + } + + logger.info("create repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType()); + + return managerDaoAdapter.create( + repositoryRole, + RepositoryRolePermissions::modify, + newRepositoryRole -> fireEvent(HandlerEventType.BEFORE_CREATE, newRepositoryRole), + newRepositoryRole -> fireEvent(HandlerEventType.CREATE, newRepositoryRole) + ); + } + + @Override + public void delete(RepositoryRole repositoryRole) { + logger.info("delete repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType()); + managerDaoAdapter.delete( + repositoryRole, + RepositoryRolePermissions::modify, + toDelete -> fireEvent(HandlerEventType.BEFORE_DELETE, toDelete), + toDelete -> fireEvent(HandlerEventType.DELETE, toDelete) + ); + } + + @Override + public void init(SCMContextProvider context) { + } + + @Override + public void modify(RepositoryRole repositoryRole) { + logger.info("modify repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType()); + managerDaoAdapter.modify( + repositoryRole, + x -> RepositoryRolePermissions.modify(), + notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, repositoryRole, notModified), + notModified -> fireEvent(HandlerEventType.MODIFY, repositoryRole, notModified)); + } + + @Override + public void refresh(RepositoryRole repositoryRole) { + logger.info("refresh repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType()); + + RepositoryRolePermissions.read().check(); + RepositoryRole fresh = repositoryRoleDAO.get(repositoryRole.getName()); + + if (fresh == null) { + throw new NotFoundException(RepositoryRole.class, repositoryRole.getName()); + } + } + + @Override + public RepositoryRole get(String id) { + RepositoryRolePermissions.read(); + + RepositoryRole repositoryRole = repositoryRoleDAO.get(id); + + if (repositoryRole != null) { + return repositoryRole.clone(); + } else { + return null; + } + } + + @Override + public Collection getAll() { + return getAll(repositoryRole -> true, null); + } + + @Override + public Collection getAll(Predicate filter, Comparator comparator) { + List repositoryRoles = new ArrayList<>(); + + if (!RepositoryRolePermissions.read().isPermitted()) { + return Collections.emptySet(); + } + for (RepositoryRole repositoryRole : repositoryRoleDAO.getAll()) { + repositoryRoles.add(repositoryRole.clone()); + } + + if (comparator != null) { + Collections.sort(repositoryRoles, comparator); + } + + return repositoryRoles; + } + + @Override + public Collection getAll(Comparator comaparator, int start, int limit) { + if (!RepositoryRolePermissions.read().isPermitted()) { + return Collections.emptySet(); + } + return Util.createSubCollection(repositoryRoleDAO.getAll(), comaparator, + (collection, item) -> { + collection.add(item.clone()); + }, start, limit); + } + + @Override + public Collection getAll(int start, int limit) + { + return getAll(null, start, limit); + } + + @Override + public Long getLastModified() + { + return repositoryRoleDAO.getLastModified(); + } + + private final RepositoryRoleDAO repositoryRoleDAO; + private final ManagerDaoAdapter managerDaoAdapter; +} From 96e7a29dce156169a35fd7cf223565eb141d646d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 6 May 2019 14:49:37 +0200 Subject: [PATCH 07/91] Add unit test for role resource --- .../RepositoryRoleCollectionResource.java | 1 - .../RepositoryRoleRootResourceTest.java | 246 ++++++++++++++++++ .../api/v2/resources/ResourceLinksMock.java | 3 +- .../resources/sonia/scm/repository/shiro.ini | 2 +- 4 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java index 12f52156f0..99d8672eec 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java @@ -5,7 +5,6 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import org.apache.shiro.authc.credential.PasswordService; import sonia.scm.repository.RepositoryRole; import sonia.scm.repository.RepositoryRoleManager; import sonia.scm.web.VndMediaType; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java new file mode 100644 index 0000000000..057658877e --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java @@ -0,0 +1,246 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.PageResult; +import sonia.scm.api.rest.JSONContextResolver; +import sonia.scm.api.rest.ObjectMapperProvider; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.web.VndMediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.util.Collections; + +import static java.net.URI.create; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; + +@SubjectAware( + username = "trillian", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +@RunWith(MockitoJUnitRunner.Silent.class) +public class RepositoryRoleRootResourceTest { + + public static final String EXISTING_ROLE = "existingRole"; + public static final RepositoryRole REPOSITORY_ROLE = new RepositoryRole("existingRole", Collections.singleton("verb"), "xml"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/")); + + @Rule + public ShiroRule shiroRule = new ShiroRule(); + + @Mock + private RepositoryRoleManager repositoryRoleManager; + + @InjectMocks + private RepositoryRoleToRepositoryRoleDtoMapperImpl roleToDtoMapper; + + @InjectMocks + private RepositoryRoleDtoToRepositoryRoleMapperImpl dtoToRoleMapper; + + private RepositoryRoleCollectionToDtoMapper collectionToDtoMapper; + + private Dispatcher dispatcher; + + @Captor + private ArgumentCaptor modifyCaptor; + @Captor + private ArgumentCaptor createCaptor; + @Captor + private ArgumentCaptor deleteCaptor; + + @Before + public void init() { + collectionToDtoMapper = new RepositoryRoleCollectionToDtoMapper(roleToDtoMapper, resourceLinks); + + RepositoryRoleCollectionResource collectionResource = new RepositoryRoleCollectionResource(repositoryRoleManager, dtoToRoleMapper, collectionToDtoMapper, resourceLinks); + RepositoryRoleResource roleResource = new RepositoryRoleResource(dtoToRoleMapper, roleToDtoMapper, repositoryRoleManager); + RepositoryRoleRootResource rootResource = new RepositoryRoleRootResource(Providers.of(collectionResource), Providers.of(roleResource)); + + doNothing().when(repositoryRoleManager).modify(modifyCaptor.capture()); + when(repositoryRoleManager.create(createCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); + doNothing().when(repositoryRoleManager).delete(deleteCaptor.capture()); + + dispatcher = createDispatcher(rootResource); + dispatcher.getProviderFactory().registerProviderInstance(new JSONContextResolver(new ObjectMapperProvider().get())); + + when(repositoryRoleManager.get(EXISTING_ROLE)).thenReturn(REPOSITORY_ROLE); + when(repositoryRoleManager.getPage(any(), any(), anyInt(), anyInt())).thenReturn(new PageResult<>(singletonList(REPOSITORY_ROLE), 1)); + } + + @Test + public void shouldGetNotFoundForNotExistingRole() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "noSuchRole"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); + } + + @Test + public void shouldGetExistingRole() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()) + .contains( + "\"name\":\"existingRole\"", + "\"verbs\":[\"verb\"]", + "\"self\":{\"href\":\"/v2/repository-roles/existingRole\"}", + "\"delete\":{\"href\":\"/v2/repository-roles/existingRole\"}" + ); + } + + @Test + @SubjectAware(username = "dent") + public void shouldNotGetDeleteLinkWithoutPermission() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()) + .doesNotContain("delete"); + } + + @Test + public void shouldUpdateRole() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole") + .contentType(VndMediaType.REPOSITORY_ROLE) + .content(content("{'name': 'existingRole', 'verbs': ['write', 'push']}")); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); + verify(repositoryRoleManager).modify(any()); + assertThat(modifyCaptor.getValue().getName()).isEqualTo("existingRole"); + assertThat(modifyCaptor.getValue().getVerbs()).containsExactly("write", "push"); + } + + @Test + public void shouldNotChangeRoleName() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole") + .contentType(VndMediaType.REPOSITORY_ROLE) + .content(content("{'name': 'changedName', 'verbs': ['write', 'push']}")); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); + verify(repositoryRoleManager, never()).modify(any()); + } + + @Test + public void shouldFailForUpdateOfNotExistingRole() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "noSuchRole") + .contentType(VndMediaType.REPOSITORY_ROLE) + .content(content("{'name': 'noSuchRole', 'verbs': ['write', 'push']}")); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); + verify(repositoryRoleManager, never()).modify(any()); + } + + @Test + public void shouldCreateRole() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2) + .contentType(VndMediaType.REPOSITORY_ROLE) + .content(content("{'name': 'newRole', 'verbs': ['write', 'push']}")); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + verify(repositoryRoleManager).create(any()); + assertThat(createCaptor.getValue().getName()).isEqualTo("newRole"); + assertThat(createCaptor.getValue().getVerbs()).containsExactly("write", "push"); + Object location = response.getOutputHeaders().getFirst("Location"); + assertThat(location).isEqualTo(create("/v2/repository-roles/newRole")); + } + + @Test + public void shouldDeleteRole() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .delete("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + EXISTING_ROLE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); + verify(repositoryRoleManager).delete(any()); + assertThat(deleteCaptor.getValue().getName()).isEqualTo(EXISTING_ROLE); + } + + @Test + public void shouldGetAllRoles() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()) + .contains( + "\"name\":\"existingRole\"", + "\"verbs\":[\"verb\"]", + "\"self\":{\"href\":\"/v2/repository-roles", + "\"delete\":{\"href\":\"/v2/repository-roles/existingRole\"}", + "\"create\":{\"href\":\"/v2/repository-roles/\"}" + ); + } + + @Test + @SubjectAware(username = "dent") + public void shouldNotGetCreateLinkWithoutPermission() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()) + .doesNotContain( + "create" + ); + } + + private byte[] content(String data) { + return data.replaceAll("'", "\"").getBytes(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 073e41a65e..b6ce087050 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -43,7 +43,8 @@ public class ResourceLinksMock { when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo)); - when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); + when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); + when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); return resourceLinks; diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini index 500325faf3..bf434508bd 100644 --- a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini +++ b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini @@ -8,7 +8,7 @@ user = secret, user [roles] admin = * -creator = repository:create +creator = repository:create,repositoryRole:read heartOfGold = "repository:read,modify,delete:hof" puzzle42 = "repository:read,write:p42" oss = "repository:pull" From e4e335b7e178439849c353d9934b894fdc3827b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 6 May 2019 15:12:58 +0200 Subject: [PATCH 08/91] Validate repository roles --- .../api/v2/resources/RepositoryRoleDto.java | 3 ++ .../RepositoryRoleRootResourceTest.java | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java index 9bd64b4ce8..50867b4f92 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java @@ -6,6 +6,7 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.validator.constraints.NotEmpty; import java.util.Collection; @@ -13,7 +14,9 @@ import java.util.Collection; @Setter @NoArgsConstructor public class RepositoryRoleDto extends HalRepresentation { + @NotEmpty private String name; + @NoBlankStrings @NotEmpty private Collection verbs; RepositoryRoleDto(Links links, Embedded embedded) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java index 057658877e..0323df27e4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java @@ -225,6 +225,48 @@ public class RepositoryRoleRootResourceTest { ); } + @Test + public void shouldFailForEmptyName() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2) + .contentType(VndMediaType.REPOSITORY_ROLE) + .content(content("{'name': '', 'verbs': ['write', 'push']}")); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); + verify(repositoryRoleManager, never()).create(any()); + } + + @Test + public void shouldFailForMissingVerbs() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2) + .contentType(VndMediaType.REPOSITORY_ROLE) + .content(content("{'name': 'ok', 'verbs': []}")); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); + verify(repositoryRoleManager, never()).create(any()); + } + + @Test + public void shouldFailForEmptyVerb() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2) + .contentType(VndMediaType.REPOSITORY_ROLE) + .content(content("{'name': 'ok', 'verbs': ['', 'push']}")); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); + verify(repositoryRoleManager, never()).create(any()); + } + @Test @SubjectAware(username = "dent") public void shouldNotGetCreateLinkWithoutPermission() throws URISyntaxException, UnsupportedEncodingException { From 45ca558101867f4bc087828a34cb00de3896c0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 6 May 2019 16:01:01 +0200 Subject: [PATCH 09/91] Get verbs from repository roles --- .../DefaultAuthorizationCollector.java | 46 ++++++++----- .../DefaultAuthorizationCollectorTest.java | 64 ++++++++++++++++--- 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index f4efd3a307..f07cdce900 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -52,7 +52,6 @@ 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; @@ -64,7 +63,6 @@ import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; import java.util.Collection; -import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -90,18 +88,19 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector /** * Constructs ... - * - * @param cacheManager + * @param cacheManager * @param repositoryDAO * @param securitySystem + * @param repositoryPermissionProvider */ @Inject public DefaultAuthorizationCollector(CacheManager cacheManager, - RepositoryDAO repositoryDAO, SecuritySystem securitySystem) + RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider) { this.cache = cacheManager.getCache(CACHE_NAME); this.repositoryDAO = repositoryDAO; this.securitySystem = securitySystem; + this.repositoryPermissionProvider = repositoryPermissionProvider; } //~--- methods -------------------------------------------------------------- @@ -201,16 +200,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector for (RepositoryPermission permission : repositoryPermissions) { hasPermission = isUserPermitted(user, groups, permission); - if (hasPermission && !permission.getVerbs().isEmpty()) - { - String perm = "repository:" + String.join(",", permission.getVerbs()) + ":" + repository.getId(); - if (logger.isTraceEnabled()) - { - logger.trace("add repository permission {} for user {} at repository {}", - perm, user.getName(), repository.getName()); - } - - builder.add(perm); + if (hasPermission) { + addRepositoryPermission(builder, repository, user, hasPermission, permission); } } @@ -226,6 +217,29 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } } + private void addRepositoryPermission(Builder builder, Repository repository, User user, boolean hasPermission, RepositoryPermission permission) { + Collection verbs = getVerbs(permission); + if (!verbs.isEmpty()) + { + String perm = "repository:" + String.join(",", verbs) + ":" + repository.getId(); + if (logger.isTraceEnabled()) + { + logger.trace("add repository permission {} for user {} at repository {}", + perm, user.getName(), repository.getName()); + } + + builder.add(perm); + } + } + + private Collection getVerbs(RepositoryPermission permission) { + return permission.getRole() == null? permission.getVerbs(): getVerbsForRole(permission.getRole()); + } + + private Collection getVerbsForRole(String roleName) { + return repositoryPermissionProvider.availableRoles().stream().filter(role -> roleName.equals(role.getName())).findFirst().orElseThrow(() -> new RuntimeException()).getVerbs(); + } + private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) { Builder builder = ImmutableSet.builder(); @@ -353,4 +367,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector /** security system */ private final SecuritySystem securitySystem; + + private final RepositoryPermissionProvider repositoryPermissionProvider; } diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index dc62119209..bf49c12005 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -33,10 +33,10 @@ package sonia.scm.security; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.hamcrest.Matchers; @@ -49,11 +49,11 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.config.ScmConfiguration; import sonia.scm.group.GroupNames; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRole; import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; @@ -90,6 +90,9 @@ public class DefaultAuthorizationCollectorTest { @Mock private SecuritySystem securitySystem; + @Mock + private RepositoryPermissionProvider repositoryPermissionProvider; + private DefaultAuthorizationCollector collector; @Rule @@ -101,11 +104,11 @@ public class DefaultAuthorizationCollectorTest { @Before public void setUp(){ when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache); - collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem); + collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider); } /** - * Tests {@link AuthorizationCollector#collect()} without user role. + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} without user role. */ @Test @SubjectAware @@ -118,7 +121,7 @@ public class DefaultAuthorizationCollectorTest { } /** - * Tests {@link AuthorizationCollector#collect()} from cache. + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} from cache. */ @Test @SubjectAware( @@ -134,7 +137,7 @@ public class DefaultAuthorizationCollectorTest { } /** - * Tests {@link AuthorizationCollector#collect()} with cache. + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with cache. */ @Test @SubjectAware( @@ -148,7 +151,7 @@ public class DefaultAuthorizationCollectorTest { } /** - * Tests {@link AuthorizationCollector#collect()} without permissions. + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} without permissions. */ @Test @SubjectAware( @@ -165,7 +168,7 @@ public class DefaultAuthorizationCollectorTest { } /** - * Tests {@link AuthorizationCollector#collect()} with repository permissions. + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with repository permissions. */ @Test @SubjectAware( @@ -191,7 +194,50 @@ public class DefaultAuthorizationCollectorTest { } /** - * Tests {@link AuthorizationCollector#collect()} with global permissions. + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} with repository roles. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectWithRepositoryRolePermissions() { + when(repositoryPermissionProvider.availableRoles()).thenReturn( + asList( + new RepositoryRole("user role", asList("user"), "xml"), + new RepositoryRole("group role", asList("group"), "xml"), + new RepositoryRole("system role", asList("system"), "system") + )); + + String group = "heart-of-gold-crew"; + authenticate(UserTestData.createTrillian(), group); + Repository heartOfGold = RepositoryTestData.createHeartOfGold(); + heartOfGold.setId("one"); + heartOfGold.setPermissions(Lists.newArrayList( + new RepositoryPermission("trillian", "user role", false), + new RepositoryPermission("trillian", "system role", false) + )); + Repository puzzle42 = RepositoryTestData.create42Puzzle(); + puzzle42.setId("two"); + RepositoryPermission permission = new RepositoryPermission(group, "group role", true); + puzzle42.setPermissions(Lists.newArrayList(permission)); + when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42)); + + // execute and assert + AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); + assertThat(authInfo.getObjectPermissions(), nullValue()); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder( + "user:autocomplete", + "group:autocomplete", + "user:changePassword:trillian", + "repository:user:one", + "repository:system:one", + "repository:group:two", + "user:read:trillian")); + } + + /** + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with global permissions. */ @Test @SubjectAware( From 0099740a222407e78fa4dfc524591c0a6cedc2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 6 May 2019 16:38:54 +0200 Subject: [PATCH 10/91] Remove permissions for deleted roles --- .../RemoveDeletedRepositoryRole.java | 48 ++++++++++ .../RemoveDeletedRepositoryRoleTest.java | 91 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 scm-core/src/main/java/sonia/scm/repository/RemoveDeletedRepositoryRole.java create mode 100644 scm-core/src/test/java/sonia/scm/repository/RemoveDeletedRepositoryRoleTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/RemoveDeletedRepositoryRole.java b/scm-core/src/main/java/sonia/scm/repository/RemoveDeletedRepositoryRole.java new file mode 100644 index 0000000000..25564dca83 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RemoveDeletedRepositoryRole.java @@ -0,0 +1,48 @@ +package sonia.scm.repository; + +import com.github.legman.Subscribe; +import sonia.scm.EagerSingleton; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; + +import java.util.Optional; + +import static sonia.scm.HandlerEventType.DELETE; + +@EagerSingleton +@Extension +public class RemoveDeletedRepositoryRole { + + private final RepositoryManager repositoryManager; + + @Inject + public RemoveDeletedRepositoryRole(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + @Subscribe + void handle(RepositoryRoleEvent event) { + if (event.getEventType() == DELETE) { + repositoryManager.getAll() + .forEach(repository -> check(repository, event.getItem())); + } + } + + private void check(Repository repository, RepositoryRole role) { + findPermission(repository, role) + .ifPresent(permission -> removeFromPermissions(repository, permission)); + } + + private Optional findPermission(Repository repository, RepositoryRole item) { + return repository.getPermissions() + .stream() + .filter(repositoryPermission -> item.getName().equals(repositoryPermission.getRole())) + .findFirst(); + } + + private void removeFromPermissions(Repository repository, RepositoryPermission permission) { + repository.removePermission(permission); + repositoryManager.modify(repository); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/RemoveDeletedRepositoryRoleTest.java b/scm-core/src/test/java/sonia/scm/repository/RemoveDeletedRepositoryRoleTest.java new file mode 100644 index 0000000000..f2f88f9ea7 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RemoveDeletedRepositoryRoleTest.java @@ -0,0 +1,91 @@ +package sonia.scm.repository; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import sonia.scm.HandlerEventType; + +import java.util.Arrays; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.quality.Strictness.LENIENT; +import static sonia.scm.HandlerEventType.DELETE; + +@ExtendWith(MockitoExtension.class) +class RemoveDeletedRepositoryRoleTest { + + static final Repository REPOSITORY = createRepositoryWithRoles("with", "deleted", "kept"); + + @Mock + RepositoryManager manager; + @Captor + ArgumentCaptor modifyCaptor; + + private RemoveDeletedRepositoryRole removeDeletedRepositoryRole; + + @BeforeEach + void init() { + removeDeletedRepositoryRole = new RemoveDeletedRepositoryRole(manager); + doNothing().when(manager).modify(modifyCaptor.capture()); + } + + @Test + void shouldRemoveDeletedPermission() { + when(manager.getAll()).thenReturn(Collections.singletonList(REPOSITORY)); + + removeDeletedRepositoryRole.handle(new RepositoryRoleEvent(DELETE, createRole("deleted"))); + + verify(manager).modify(any()); + Assertions.assertThat(modifyCaptor.getValue().getPermissions()) + .containsExactly(createPermission("kept")); + } + + @Test + @MockitoSettings(strictness = LENIENT) + void shouldDoNothingForEventsWithUnusedRole() { + when(manager.getAll()).thenReturn(Collections.singletonList(REPOSITORY)); + + removeDeletedRepositoryRole.handle(new RepositoryRoleEvent(DELETE, createRole("unused"))); + + verify(manager, never()).modify(any()); + } + + @Test + @MockitoSettings(strictness = LENIENT) + void shouldDoNothingForEventsOtherThanDelete() { + when(manager.getAll()).thenReturn(Collections.singletonList(REPOSITORY)); + + Arrays.stream(HandlerEventType.values()) + .filter(type -> type != DELETE) + .forEach( + type -> removeDeletedRepositoryRole.handle(new RepositoryRoleEvent(type, createRole("deleted"))) + ); + + verify(manager, never()).modify(any()); + } + + private RepositoryRole createRole(String name) { + return new RepositoryRole(name, Collections.singleton("x"), "x"); + } + + static Repository createRepositoryWithRoles(String name, String... roles) { + Repository repository = new Repository("x", "git", "space", name); + Arrays.stream(roles).forEach(role -> repository.addPermission(createPermission(role))); + return repository; + } + + private static RepositoryPermission createPermission(String role) { + return new RepositoryPermission("user", role, false); + } +} From 864589075881e998f7a991e1d9736fc7703902b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 6 May 2019 16:45:35 +0200 Subject: [PATCH 11/91] Cleanup --- .../DefaultAuthorizationCollector.java | 11 ++++-- .../DefaultAuthorizationCollectorTest.java | 35 +++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index f07cdce900..baff3b951c 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -201,7 +201,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector { hasPermission = isUserPermitted(user, groups, permission); if (hasPermission) { - addRepositoryPermission(builder, repository, user, hasPermission, permission); + addRepositoryPermission(builder, repository, user, permission); } } @@ -217,7 +217,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } } - private void addRepositoryPermission(Builder builder, Repository repository, User user, boolean hasPermission, RepositoryPermission permission) { + private void addRepositoryPermission(Builder builder, Repository repository, User user, RepositoryPermission permission) { Collection verbs = getVerbs(permission); if (!verbs.isEmpty()) { @@ -237,7 +237,12 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private Collection getVerbsForRole(String roleName) { - return repositoryPermissionProvider.availableRoles().stream().filter(role -> roleName.equals(role.getName())).findFirst().orElseThrow(() -> new RuntimeException()).getVerbs(); + return repositoryPermissionProvider.availableRoles() + .stream() + .filter(role -> roleName.equals(role.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("unknown role: " + roleName)) + .getVerbs(); } private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) { diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index bf49c12005..8e7cb8a70e 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -58,7 +58,10 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import java.util.Collections; + import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; @@ -203,9 +206,9 @@ public class DefaultAuthorizationCollectorTest { public void testCollectWithRepositoryRolePermissions() { when(repositoryPermissionProvider.availableRoles()).thenReturn( asList( - new RepositoryRole("user role", asList("user"), "xml"), - new RepositoryRole("group role", asList("group"), "xml"), - new RepositoryRole("system role", asList("system"), "system") + new RepositoryRole("user role", singletonList("user"), "xml"), + new RepositoryRole("group role", singletonList("group"), "xml"), + new RepositoryRole("system role", singletonList("system"), "system") )); String group = "heart-of-gold-crew"; @@ -236,6 +239,32 @@ public class DefaultAuthorizationCollectorTest { "user:read:trillian")); } + /** + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} with repository roles. + */ + @Test(expected = IllegalStateException.class) + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectWithUnknownRepositoryRole() { + when(repositoryPermissionProvider.availableRoles()).thenReturn( + singletonList( + new RepositoryRole("something", singletonList("something"), "xml") + )); + + String group = "heart-of-gold-crew"; + authenticate(UserTestData.createTrillian(), group); + Repository heartOfGold = RepositoryTestData.createHeartOfGold(); + heartOfGold.setId("one"); + heartOfGold.setPermissions(singletonList( + new RepositoryPermission("trillian", "unknown", false) + )); + when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold)); + + // execute and assert + AuthorizationInfo authInfo = collector.collect(); + } + /** * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with global permissions. */ From c5b20566c1d66c1036bd5e564194ddbd9fc7aadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 7 May 2019 09:20:19 +0200 Subject: [PATCH 12/91] Fix bugs with integration test --- .../scm/repository/RepositoryPermission.java | 2 +- .../repository/xml/XmlRepositoryRoleDAO.java | 2 + .../xml/XmlRepositoryRoleDatabase.java | 2 +- .../test/java/sonia/scm/it/RoleITCase.java | 71 +++++++++++++++++++ .../java/sonia/scm/it/utils/TestData.java | 25 +++++-- .../RepositoryPermissionProvider.java | 22 +----- 6 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 scm-it/src/test/java/sonia/scm/it/RoleITCase.java 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 6bd95c9f3d..3099c1f74b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -144,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(), role, groupPermission); + return Objects.hashCode(name, verbs == null? -1: verbs.size(), role, groupPermission); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java index c913d57ee4..0eb860a134 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java @@ -6,8 +6,10 @@ import sonia.scm.repository.RepositoryRoleDAO; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.xml.AbstractXmlDAO; +import javax.inject.Singleton; import java.util.List; +@Singleton public class XmlRepositoryRoleDAO extends AbstractXmlDAO implements RepositoryRoleDAO { diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java index 8219d32a67..c7665316e2 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java @@ -21,7 +21,7 @@ public class XmlRepositoryRoleDatabase implements XmlDatabase { @XmlJavaTypeAdapter(XmlRepositoryRoleMapAdapter.class) @XmlElement(name = "roles") - private Map roleMap = new LinkedHashMap(); + private Map roleMap = new LinkedHashMap<>(); public XmlRepositoryRoleDatabase() { long c = System.currentTimeMillis(); diff --git a/scm-it/src/test/java/sonia/scm/it/RoleITCase.java b/scm-it/src/test/java/sonia/scm/it/RoleITCase.java new file mode 100644 index 0000000000..998c4e315e --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RoleITCase.java @@ -0,0 +1,71 @@ +package sonia.scm.it; + +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.it.utils.RestUtil; +import sonia.scm.it.utils.TestData; +import sonia.scm.web.VndMediaType; + +import static org.junit.Assert.assertNotNull; +import static sonia.scm.it.PermissionsITCase.USER_PASS; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; +import static sonia.scm.it.utils.TestData.callRepository; + +public class RoleITCase { + + private static final String USER = "user"; + public static final String ROLE_NAME = "permission-role"; + + @Before + public void init() { + TestData.createDefault(); + TestData.createNotAdminUser(USER, USER_PASS); + } + + @Test + public void userShouldSeePermissionsAfterAddingRoleToUser() { + callRepository(USER, USER_PASS, "git", HttpStatus.SC_FORBIDDEN); + + given() + .when() + .delete(RestUtil.createResourceUrl("repository-roles/" + ROLE_NAME)) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + given(VndMediaType.REPOSITORY_ROLE) + .when() + .content("{" + + "\"name\": \"" + ROLE_NAME + "\"," + + "\"verbs\": [\"read\",\"permissionRead\"]" + + "}") + .post(RestUtil.createResourceUrl("repository-roles/")) + .then() + .statusCode(HttpStatus.SC_CREATED); + + String permissionUrl = given(VndMediaType.REPOSITORY, USER_SCM_ADMIN, USER_SCM_ADMIN) + .when() + .get(TestData.getDefaultRepositoryUrl("git")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href"); + + given(VndMediaType.REPOSITORY_PERMISSION) + .when() + .content("{\n" + + "\t\"role\": \"" + ROLE_NAME + "\",\n" + + "\t\"name\": \"" + USER + "\",\n" + + "\t\"groupPermission\": false\n" + + "\t\n" + + "}") + .post(permissionUrl) + .then() + .statusCode(HttpStatus.SC_CREATED); + + assertNotNull(callRepository(USER, USER_PASS, "git", HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 23228d686b..20de47ffa4 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -106,14 +106,31 @@ public class TestData { ; } - public static void createUserPermission(String name, Collection permissionType, String repositoryType) { + public static void createUserPermission(String username, Collection verbs, String repositoryType) { String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); - LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl); + LOG.info("create permission with name {} and verbs {} using the endpoint: {}", username, verbs, defaultPermissionUrl); given(VndMediaType.REPOSITORY_PERMISSION) .when() .content("{\n" + - "\t\"verbs\": " + permissionType.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" + - "\t\"name\": \"" + name + "\",\n" + + "\t\"verbs\": " + verbs.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" + + "\t\"name\": \"" + username + "\",\n" + + "\t\"groupPermission\": false\n" + + "\t\n" + + "}") + .post(defaultPermissionUrl) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } + + public static void createUserPermission(String username, String roleName, String repositoryType) { + String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); + LOG.info("create permission with name {} and role {} using the endpoint: {}", username, roleName, defaultPermissionUrl); + given(VndMediaType.REPOSITORY_PERMISSION) + .when() + .content("{\n" + + "\t\"role\": " + roleName + ",\n" + + "\t\"name\": \"" + username + "\",\n" + "\t\"groupPermission\": false\n" + "\t\n" + "}") diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java index 292feee7c1..bacd9fb637 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java @@ -1,30 +1,12 @@ package sonia.scm.security; import com.google.inject.Inject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.RepositoryRole; import sonia.scm.repository.RepositoryRoleDAO; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import java.io.IOException; -import java.net.URL; import java.util.AbstractList; -import java.util.ArrayList; import java.util.Collection; -import java.util.Enumeration; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static java.util.Collections.unmodifiableCollection; +import java.util.List ; public class RepositoryPermissionProvider { @@ -42,8 +24,8 @@ public class RepositoryPermissionProvider { } public Collection availableRoles() { - List customRoles = repositoryRoleDAO.getAll(); List availableSystemRoles = systemRepositoryPermissionProvider.availableRoles(); + List customRoles = repositoryRoleDAO.getAll(); return new AbstractList() { @Override From de59f5657ff68540968db7f147db01a16da438c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 7 May 2019 15:20:54 +0200 Subject: [PATCH 13/91] Remove duplicate verbs --- .../security/SystemRepositoryPermissionProvider.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java index 63fdeec702..0350698352 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java @@ -21,10 +21,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import static java.util.Collections.unmodifiableCollection; -import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.toList; class SystemRepositoryPermissionProvider { @@ -36,8 +35,8 @@ class SystemRepositoryPermissionProvider { @Inject public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) { AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader); - this.availableVerbs = unmodifiableList(new ArrayList<>(availablePermissions.availableVerbs)); - this.availableRoles = unmodifiableList(new ArrayList<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs, "system")).collect(Collectors.toList()))); + this.availableVerbs = removeDuplicates(availablePermissions.availableVerbs); + this.availableRoles = removeDuplicates(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs, "system")).collect(toList())); } public List availableVerbs() { @@ -109,6 +108,10 @@ class SystemRepositoryPermissionProvider { } } + private static List removeDuplicates(Collection items) { + return items.stream().distinct().collect(toList()); + } + private static class AvailableRepositoryPermissions { private final Collection availableVerbs; private final Collection availableRoles; From ddaaa1dbe93fd1c737783ab9697b98bf0d121c24 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 7 May 2019 16:49:36 +0200 Subject: [PATCH 14/91] corrected DELETE action const --- scm-ui/src/groups/modules/groups.js | 2 +- scm-ui/src/users/modules/users.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index cb3c24aa0f..6884a8817f 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -28,7 +28,7 @@ export const MODIFY_GROUP_SUCCESS = `${MODIFY_GROUP}_${types.SUCCESS_SUFFIX}`; export const MODIFY_GROUP_FAILURE = `${MODIFY_GROUP}_${types.FAILURE_SUFFIX}`; export const MODIFY_GROUP_RESET = `${MODIFY_GROUP}_${types.RESET_SUFFIX}`; -export const DELETE_GROUP = "scm/groups/DELETE"; +export const DELETE_GROUP = "scm/groups/DELETE_GROUP"; export const DELETE_GROUP_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`; export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`; export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`; diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index c83a4d9b94..71235c689c 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -28,7 +28,7 @@ export const MODIFY_USER_SUCCESS = `${MODIFY_USER}_${types.SUCCESS_SUFFIX}`; export const MODIFY_USER_FAILURE = `${MODIFY_USER}_${types.FAILURE_SUFFIX}`; export const MODIFY_USER_RESET = `${MODIFY_USER}_${types.RESET_SUFFIX}`; -export const DELETE_USER = "scm/users/DELETE"; +export const DELETE_USER = "scm/users/DELETE_USER"; export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`; export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`; export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`; From 91b957f6ccf595fd76ac0a83e4ddf34c25b8efa2 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 7 May 2019 16:50:18 +0200 Subject: [PATCH 15/91] sorted imports --- scm-ui/src/users/modules/users.test.js | 70 +++++++++++++------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/scm-ui/src/users/modules/users.test.js b/scm-ui/src/users/modules/users.test.js index ac4d1c97a9..e046443f3b 100644 --- a/scm-ui/src/users/modules/users.test.js +++ b/scm-ui/src/users/modules/users.test.js @@ -4,49 +4,49 @@ import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; import reducer, { - CREATE_USER_FAILURE, - CREATE_USER_PENDING, - CREATE_USER_SUCCESS, - createUser, - DELETE_USER_FAILURE, - DELETE_USER_PENDING, - DELETE_USER_SUCCESS, - deleteUser, - deleteUserSuccess, - FETCH_USER_FAILURE, - FETCH_USER_PENDING, - isFetchUserPending, - FETCH_USER_SUCCESS, - FETCH_USERS_FAILURE, + FETCH_USERS, FETCH_USERS_PENDING, FETCH_USERS_SUCCESS, + FETCH_USERS_FAILURE, + FETCH_USER, + FETCH_USER_PENDING, + FETCH_USER_SUCCESS, + FETCH_USER_FAILURE, + CREATE_USER, + CREATE_USER_PENDING, + CREATE_USER_SUCCESS, + CREATE_USER_FAILURE, + MODIFY_USER, + MODIFY_USER_PENDING, + MODIFY_USER_SUCCESS, + MODIFY_USER_FAILURE, + DELETE_USER, + DELETE_USER_PENDING, + DELETE_USER_SUCCESS, + DELETE_USER_FAILURE, + fetchUsers, + getFetchUsersFailure, + getUsersFromState, + isFetchUsersPending, + fetchUsersSuccess, fetchUserByLink, fetchUserByName, fetchUserSuccess, + isFetchUserPending, getFetchUserFailure, - fetchUsers, - fetchUsersSuccess, - isFetchUsersPending, - selectListAsCollection, - isPermittedToCreateUsers, - MODIFY_USER, - MODIFY_USER_FAILURE, - MODIFY_USER_PENDING, - MODIFY_USER_SUCCESS, - modifyUser, - getUsersFromState, - FETCH_USERS, - getFetchUsersFailure, - FETCH_USER, - CREATE_USER, + createUser, isCreateUserPending, getCreateUserFailure, getUserByName, + modifyUser, isModifyUserPending, getModifyUserFailure, - DELETE_USER, + deleteUser, isDeleteUserPending, - getDeleteUserFailure + deleteUserSuccess, + getDeleteUserFailure, + selectListAsCollection, + isPermittedToCreateUsers } from "./users"; const userZaphod = { @@ -302,7 +302,7 @@ describe("users fetch()", () => { }); it("should fail updating user on HTTP 500", () => { - fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", { + fetchMock.putOnce(USER_ZAPHOD_URL, { status: 500 }); @@ -316,7 +316,7 @@ describe("users fetch()", () => { }); it("should delete successfully user zaphod", () => { - fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", { + fetchMock.deleteOnce(USER_ZAPHOD_URL, { status: 204 }); @@ -331,7 +331,7 @@ describe("users fetch()", () => { }); it("should call the callback, after successful delete", () => { - fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", { + fetchMock.deleteOnce(USER_ZAPHOD_URL, { status: 204 }); @@ -347,7 +347,7 @@ describe("users fetch()", () => { }); it("should fail to delete user zaphod", () => { - fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", { + fetchMock.deleteOnce(USER_ZAPHOD_URL, { status: 500 }); From 54c81ca502e8ae9fdb31e9444da041e429f037c8 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 7 May 2019 16:50:44 +0200 Subject: [PATCH 16/91] started implementing tests --- scm-ui/src/config/modules/roles.test.js | 552 ++++++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 scm-ui/src/config/modules/roles.test.js diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/modules/roles.test.js new file mode 100644 index 0000000000..7bb6bdcc60 --- /dev/null +++ b/scm-ui/src/config/modules/roles.test.js @@ -0,0 +1,552 @@ +// @flow +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import fetchMock from "fetch-mock"; + +import reducer, { + FETCH_ROLES, + FETCH_ROLES_PENDING, + FETCH_ROLES_SUCCESS, + FETCH_ROLES_FAILURE, + FETCH_ROLE, + FETCH_ROLE_PENDING, + FETCH_ROLE_SUCCESS, + FETCH_ROLE_FAILURE, + CREATE_ROLE, + CREATE_ROLE_PENDING, + CREATE_ROLE_SUCCESS, + CREATE_ROLE_FAILURE, + MODIFY_ROLE, + MODIFY_ROLE_PENDING, + MODIFY_ROLE_SUCCESS, + MODIFY_ROLE_FAILURE, + DELETE_ROLE, + DELETE_ROLE_PENDING, + DELETE_ROLE_SUCCESS, + DELETE_ROLE_FAILURE, + fetchRoles, + getFetchRolesFailure, + getRolesFromState, + isFetchRolesPending, + fetchRolesSuccess, + fetchRoleByLink, + fetchRoleByName, + fetchRoleSuccess, + isFetchRolePending, + getFetchRoleFailure, + createRole, + isCreateRolePending, + getCreateRoleFailure, + getRoleByName, + modifyRole, + isModifyRolePending, + getModifyRoleFailure, + deleteRole, + isDeleteRolePending, + deleteRoleSuccess, + getDeleteRoleFailure, + selectListAsCollection, + isPermittedToCreateRoles +} from "./roles"; + +const URL = "roles"; +const ROLES_URL = "api/v2/repositoryPermissions"; + +const error = new Error("FEHLER!"); + +const verbs = [ + "createPullRequest", + "readPullRequest", + "commentPullRequest", + "modifyPullRequest", + "mergePullRequest", + "git", + "hg", + "read", + "modify", + "delete", + "pull", + "push", + "permissionRead", + "permissionWrite", + "*" +]; + +const repositoryRoles = { + availableVerbs: verbs, + availableRoles: [ + { + name: "READ", + creationDate: null, + lastModified: null, + type: "system", + verb: ["read", "pull", "readPullRequest"] + }, + { + name: "WRITE", + creationDate: null, + lastModified: null, + type: "system", + verb: [ + "read", + "pull", + "push", + "createPullRequest", + "readPullRequest", + "commentPullRequest", + "mergePullRequest" + ] + }, + { + name: "OWNER", + creationDate: null, + lastModified: null, + type: "system", + verb: ["*"] + } + ], + _links: { + self: { + href: "http://localhost:8081/scm/api/v2/repositoryPermissions/" + } + } +}; + +const responseBody = { + entries: repositoryRoles, + rolesUpdatePermission: false +}; + +const response = { + headers: { "content-type": "application/json" }, + responseBody +}; + +describe("repository roles fetch()", () => { + const mockStore = configureMockStore([thunk]); + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should successfully fetch repository roles", () => { + fetchMock.getOnce(ROLES_URL, response); + + const expectedActions = [ + { type: FETCH_ROLES_PENDING }, + { + type: FETCH_ROLES_SUCCESS, + payload: response + } + ]; + + const store = mockStore({}); + + return store.dispatch(fetchRoles(URL)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should fail getting repository roles on HTTP 500", () => { + fetchMock.getOnce(ROLES_URL, { + status: 500 + }); + + const store = mockStore({}); + + return store.dispatch(fetchRoles(URL)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_ROLES_PENDING); + expect(actions[1].type).toEqual(FETCH_ROLES_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should add a role successfully", () => { + // unmatched + fetchMock.postOnce(ROLES_URL, { + status: 204 + }); + + // after create, the roles are fetched again + fetchMock.getOnce(ROLES_URL, response); + + const store = mockStore({}); + + return store.dispatch(createRole(URL, role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(CREATE_ROLE_PENDING); + expect(actions[1].type).toEqual(CREATE_ROLE_SUCCESS); + }); + }); + + it("should fail adding a role on HTTP 500", () => { + fetchMock.postOnce(ROLES_URL, { + status: 500 + }); + + const store = mockStore({}); + + return store.dispatch(createRole(URL, role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(CREATE_ROLE_PENDING); + expect(actions[1].type).toEqual(CREATE_ROLE_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should call the callback after role successfully created", () => { + // unmatched + fetchMock.postOnce(ROLES_URL, { + status: 204 + }); + + let callMe = "not yet"; + + const callback = () => { + callMe = "yeah"; + }; + + const store = mockStore({}); + return store.dispatch(createRole(URL, role1, callback)).then(() => { + expect(callMe).toBe("yeah"); + }); + }); + + it("successfully update role", () => { + fetchMock.putOnce(ROLE1_URL, { + status: 204 + }); + fetchMock.getOnce(ROLE1_URL, role1); + + const store = mockStore({}); + return store.dispatch(modifyRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions.length).toBe(3); + expect(actions[0].type).toEqual(MODIFY_ROLE_PENDING); + expect(actions[1].type).toEqual(MODIFY_ROLE_SUCCESS); + expect(actions[2].type).toEqual(FETCH_ROLE_PENDING); + }); + }); + + it("should call callback, after successful modified role", () => { + fetchMock.putOnce(ROLE1_URL, { + status: 204 + }); + fetchMock.getOnce(ROLE1_URL, role1); + + let called = false; + const callMe = () => { + called = true; + }; + + const store = mockStore({}); + return store.dispatch(modifyRole(role1, callMe)).then(() => { + expect(called).toBeTruthy(); + }); + }); + + + + + + it("should fail updating role on HTTP 500", () => { + fetchMock.putOnce(ROLE1_URL, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(modifyRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_ROLE_PENDING); + expect(actions[1].type).toEqual(MODIFY_ROLE_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should delete successfully role1", () => { + fetchMock.deleteOnce(ROLE1_URL, { + status: 204 + }); + + const store = mockStore({}); + return store.dispatch(deleteRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions.length).toBe(2); + expect(actions[0].type).toEqual(DELETE_ROLE_PENDING); + expect(actions[0].payload).toBe(role1); + expect(actions[1].type).toEqual(DELETE_ROLE_SUCCESS); + }); + }); + + it("should call the callback, after successful delete", () => { + fetchMock.deleteOnce(ROLE1_URL, { + status: 204 + }); + + let called = false; + const callMe = () => { + called = true; + }; + + const store = mockStore({}); + return store.dispatch(deleteRole(role1, callMe)).then(() => { + expect(called).toBeTruthy(); + }); + }); + + it("should fail to delete role1", () => { + fetchMock.deleteOnce(ROLE1_URL, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(deleteRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(DELETE_ROLE_PENDING); + expect(actions[0].payload).toBe(role1); + expect(actions[1].type).toEqual(DELETE_ROLE_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); +}); + +describe("repository roles reducer", () => { + it("should update state correctly according to FETCH_ROLES_SUCCESS action", () => { + + }); + + it("should set roleCreatePermission to true if update link is present", () => { + + }); + + it("should not replace whole byNames map when fetching roles", () => { + + }); + + it("should remove role from state when delete succeeds", () => { + + }); + + it("should set roleCreatePermission to true if create link is present", () => { + + }); + + it("should update state according to FETCH_ROLE_SUCCESS action", () => { + + }); + + it("should affect roles state nor the state of other roles", () => { + + }); +}); + +describe("selector tests", () => { + it("should return an empty object", () => { + expect(selectListAsCollection({})).toEqual({}); + expect(selectListAsCollection({ roles: { a: "a" } })).toEqual({}); + }); + + it("should return a state slice collection", () => { + const collection = { + page: 3, + totalPages: 42 + }; + + const state = { + roles: { + list: { + entry: collection + } + } + }; + expect(selectListAsCollection(state)).toBe(collection); + }); + + it("should return false", () => { + expect(isPermittedToCreateRoles({})).toBe(false); + expect(isPermittedToCreateRoles({ roles: { list: { entry: {} } } })).toBe( + false + ); + expect( + isPermittedToCreateRoles({ + roles: { list: { entry: { roleCreatePermission: false } } } + }) + ).toBe(false); + }); + + it("should return true", () => { + const state = { + roles: { + list: { + entry: { + roleCreatePermission: true + } + } + } + }; + expect(isPermittedToCreateRoles(state)).toBe(true); + }); + + it("should get roles from state", () => { + const state = { + roles: { + list: { + entries: ["a", "b"] + }, + byNames: { + a: { name: "a" }, + b: { name: "b" } + } + } + }; + expect(getRolesFromState(state)).toEqual([{ name: "a" }, { name: "b" }]); + }); + + it("should return true, when fetch roles is pending", () => { + const state = { + pending: { + [FETCH_ROLES]: true + } + }; + expect(isFetchRolesPending(state)).toEqual(true); + }); + + it("should return false, when fetch roles is not pending", () => { + expect(isFetchRolesPending({})).toEqual(false); + }); + + it("should return error when fetch roles did fail", () => { + const state = { + failure: { + [FETCH_ROLES]: error + } + }; + expect(getFetchRolesFailure(state)).toEqual(error); + }); + + it("should return undefined when fetch roles did not fail", () => { + expect(getFetchRolesFailure({})).toBe(undefined); + }); + + it("should return true if create role is pending", () => { + const state = { + pending: { + [CREATE_ROLE]: true + } + }; + expect(isCreateRolePending(state)).toBe(true); + }); + + it("should return false if create role is not pending", () => { + const state = { + pending: { + [CREATE_ROLE]: false + } + }; + expect(isCreateRolePending(state)).toBe(false); + }); + + it("should return error when create role did fail", () => { + const state = { + failure: { + [CREATE_ROLE]: error + } + }; + expect(getCreateRoleFailure(state)).toEqual(error); + }); + + it("should return undefined when create role did not fail", () => { + expect(getCreateRoleFailure({})).toBe(undefined); + }); + + it("should return role1", () => { + const state = { + roles: { + byNames: { + role1: role1 + } + } + }; + expect(getRoleByName(state, "role1")).toEqual(role1); + }); + + it("should return true, when fetch role2 is pending", () => { + const state = { + pending: { + [FETCH_ROLE + "/role2"]: true + } + }; + expect(isFetchRolePending(state, "role2")).toEqual(true); + }); + + it("should return false, when fetch role2 is not pending", () => { + expect(isFetchRolePending({}, "role2")).toEqual(false); + }); + + it("should return error when fetch role2 did fail", () => { + const state = { + failure: { + [FETCH_ROLE + "/role2"]: error + } + }; + expect(getFetchRoleFailure(state, "role2")).toEqual(error); + }); + + it("should return undefined when fetch role2 did not fail", () => { + expect(getFetchRoleFailure({}, "role2")).toBe(undefined); + }); + + it("should return true, when modify role1 is pending", () => { + const state = { + pending: { + [MODIFY_ROLE + "/role1"]: true + } + }; + expect(isModifyRolePending(state, "role1")).toEqual(true); + }); + + it("should return false, when modify role1 is not pending", () => { + expect(isModifyRolePending({}, "role1")).toEqual(false); + }); + + it("should return error when modify role1 did fail", () => { + const state = { + failure: { + [MODIFY_ROLE + "/role1"]: error + } + }; + expect(getModifyRoleFailure(state, "role1")).toEqual(error); + }); + + it("should return undefined when modify role1 did not fail", () => { + expect(getModifyRoleFailure({}, "role1")).toBe(undefined); + }); + + it("should return true, when delete role2 is pending", () => { + const state = { + pending: { + [DELETE_ROLE + "/role2"]: true + } + }; + expect(isDeleteRolePending(state, "role2")).toEqual(true); + }); + + it("should return false, when delete role2 is not pending", () => { + expect(isDeleteRolePending({}, "role2")).toEqual(false); + }); + + it("should return error when delete role2 did fail", () => { + const state = { + failure: { + [DELETE_ROLE + "/role2"]: error + } + }; + expect(getDeleteRoleFailure(state, "role2")).toEqual(error); + }); + + it("should return undefined when delete role2 did not fail", () => { + expect(getDeleteRoleFailure({}, "role2")).toBe(undefined); + }); +}); + From c79da7cc3959b31197ec9bc619138c353f548a15 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 7 May 2019 16:51:16 +0200 Subject: [PATCH 17/91] started implementing modules --- scm-ui/src/config/modules/roles.js | 477 +++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 scm-ui/src/config/modules/roles.js diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js new file mode 100644 index 0000000000..823fbb7aa7 --- /dev/null +++ b/scm-ui/src/config/modules/roles.js @@ -0,0 +1,477 @@ +// @flow +import { apiClient } from "@scm-manager/ui-components"; +import { isPending } from "../../modules/pending"; +import { getFailure } from "../../modules/failure"; +import * as types from "../../modules/types"; +import { combineReducers, Dispatch } from "redux"; +import type {Action, PagedCollection, Role} from "@scm-manager/ui-types"; + +export const FETCH_ROLES = "scm/roles/FETCH_ROLES"; +export const FETCH_ROLES_PENDING = `${FETCH_ROLES}_${types.PENDING_SUFFIX}`; +export const FETCH_ROLES_SUCCESS = `${FETCH_ROLES}_${types.SUCCESS_SUFFIX}`; +export const FETCH_ROLES_FAILURE = `${FETCH_ROLES}_${types.FAILURE_SUFFIX}`; + +export const FETCH_ROLE = "scm/roles/FETCH_ROLE"; +export const FETCH_ROLE_PENDING = `${FETCH_ROLE}_${types.PENDING_SUFFIX}`; +export const FETCH_ROLE_SUCCESS = `${FETCH_ROLE}_${types.SUCCESS_SUFFIX}`; +export const FETCH_ROLE_FAILURE = `${FETCH_ROLE}_${types.FAILURE_SUFFIX}`; + +export const CREATE_ROLE = "scm/roles/CREATE_ROLE"; +export const CREATE_ROLE_PENDING = `${CREATE_ROLE}_${types.PENDING_SUFFIX}`; +export const CREATE_ROLE_SUCCESS = `${CREATE_ROLE}_${types.SUCCESS_SUFFIX}`; +export const CREATE_ROLE_FAILURE = `${CREATE_ROLE}_${types.FAILURE_SUFFIX}`; +export const CREATE_ROLE_RESET = `${CREATE_ROLE}_${types.RESET_SUFFIX}`; + +export const MODIFY_ROLE = "scm/roles/MODIFY_ROLE"; +export const MODIFY_ROLE_PENDING = `${MODIFY_ROLE}_${types.PENDING_SUFFIX}`; +export const MODIFY_ROLE_SUCCESS = `${MODIFY_ROLE}_${types.SUCCESS_SUFFIX}`; +export const MODIFY_ROLE_FAILURE = `${MODIFY_ROLE}_${types.FAILURE_SUFFIX}`; +export const MODIFY_ROLE_RESET = `${MODIFY_ROLE}_${types.RESET_SUFFIX}`; + +export const DELETE_ROLE = "scm/roles/DELETE_ROLE"; +export const DELETE_ROLE_PENDING = `${DELETE_ROLE}_${types.PENDING_SUFFIX}`; +export const DELETE_ROLE_SUCCESS = `${DELETE_ROLE}_${types.SUCCESS_SUFFIX}`; +export const DELETE_ROLE_FAILURE = `${DELETE_ROLE}_${types.FAILURE_SUFFIX}`; + +const CONTENT_TYPE_ROLE = "application/vnd.scmm-role+json;v=2"; + +// fetch roles +export function fetchRolesPending(): Action { + return { + type: FETCH_ROLES_PENDING + }; +} + +export function fetchRolesSuccess(roles: any): Action { + return { + type: FETCH_ROLES_SUCCESS, + payload: roles + }; +} + +export function fetchRolesFailure(url: string, error: Error): Action { + return { + type: FETCH_ROLES_FAILURE, + payload: { + error, + url + } + }; +} + +export function fetchRolesByLink(link: string) { + return function(dispatch: any) { + dispatch(fetchRolesPending()); + return apiClient + .get(link) + .then(response => response.json()) + .then(data => { + dispatch(fetchRolesSuccess(data)); + }) + .catch(error => { + dispatch(fetchRolesFailure(link, error)); + }); + }; +} + +export function fetchRoles(link: string) { + return fetchRolesByLink(link); +} + +export function fetchRolesByPage(link: string, page: number, filter?: string) { + // backend start counting by 0 + if (filter) { + return fetchRolesByLink( + `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` + ); + } + return fetchRolesByLink(`${link}?page=${page - 1}`); +} + +// fetch role +export function fetchRolePending(name: string): Action { + return { + type: FETCH_ROLE_PENDING, + payload: name, + itemId: name + }; +} + +export function fetchRoleSuccess(role: any): Action { + return { + type: FETCH_ROLE_SUCCESS, + payload: role, + itemId: role.name + }; +} + +export function fetchRoleFailure(name: string, error: Error): Action { + return { + type: FETCH_ROLE_FAILURE, + payload: { + name, + error + }, + itemId: name + }; +} + +function fetchRole(link: string, name: string) { + return function(dispatch: any) { + dispatch(fetchRolePending(name)); + return apiClient + .get(link) + .then(response => { + return response.json(); + }) + .then(data => { + dispatch(fetchRoleSuccess(data)); + }) + .catch(error => { + dispatch(fetchRoleFailure(name, error)); + }); + }; +} + +export function fetchRoleByName(link: string, name: string) { + const roleUrl = link.endsWith("/") ? link + name : link + "/" + name; + return fetchRole(roleUrl, name); +} + +export function fetchRoleByLink(role: Role) { + return fetchRole(role._links.self.href, role.name); +} + +// create role +export function createRolePending(role: Role): Action { + return { + type: CREATE_ROLE_PENDING, + role + }; +} + +export function createRoleSuccess(): Action { + return { + type: CREATE_ROLE_SUCCESS + }; +} + +export function createRoleFailure(error: Error): Action { + return { + type: CREATE_ROLE_FAILURE, + payload: error + }; +} + +export function createRoleReset() { + return { + type: CREATE_ROLE_RESET + }; +} + +export function createRole(link: string, role: Role, callback?: () => void) { + return function(dispatch: Dispatch) { + dispatch(createRolePending(role)); + return apiClient + .post(link, role, CONTENT_TYPE_ROLE) + .then(() => { + dispatch(createRoleSuccess()); + if (callback) { + callback(); + } + }) + .catch(error => dispatch(createRoleFailure(error))); + }; +} + +// modify group +export function modifyRolePending(role: Role): Action { + return { + type: MODIFY_ROLE_PENDING, + payload: role, + itemId: role.name + }; +} + +export function modifyRoleSuccess(role: Role): Action { + return { + type: MODIFY_ROLE_SUCCESS, + payload: role, + itemId: role.name + }; +} + +export function modifyRoleFailure(role: Role, error: Error): Action { + return { + type: MODIFY_ROLE_FAILURE, + payload: { + error, + role + }, + itemId: role.name + }; +} + +export function modifyRoleReset(role: Role): Action { + return { + type: MODIFY_ROLE_RESET, + itemId: role.name + }; +} + +export function modifyRole(role: Role, callback?: () => void) { + return function(dispatch: Dispatch) { + dispatch(modifyRolePending(role)); + return apiClient + .put(role._links.update.href, role, CONTENT_TYPE_ROLE) + .then(() => { + dispatch(modifyRoleSuccess(role)); + if (callback) { + callback(); + } + }) + .then(() => { + dispatch(fetchRoleByLink(role)); + }) + .catch(err => { + dispatch(modifyRoleFailure(role, err)); + }); + }; +} + +// delete role +export function deleteRolePending(role: Role): Action { + return { + type: DELETE_ROLE_PENDING, + payload: role, + itemId: role.name + }; +} + +export function deleteRoleSuccess(role: Role): Action { + return { + type: DELETE_ROLE_SUCCESS, + payload: role, + itemId: role.name + }; +} + +export function deleteRoleFailure(role: Role, error: Error): Action { + return { + type: DELETE_ROLE_FAILURE, + payload: { + error, + role + }, + itemId: role.name + }; +} + +export function deleteRole(role: Role, callback?: () => void) { + return function(dispatch: any) { + dispatch(deleteRolePending(role)); + return apiClient + .delete(role._links.delete.href) + .then(() => { + dispatch(deleteRoleSuccess(role)); + if (callback) { + callback(); + } + }) + .catch(error => { + dispatch(deleteRoleFailure(role, error)); + }); + }; +} + +function extractRolesByNames( + roles: Role[], + roleNames: string[], + oldRolesByNames: Object +) { + const rolesByNames = {}; + + for (let role of roles) { + rolesByNames[role.name] = role; + } + + for (let roleName in oldRolesByNames) { + rolesByNames[roleName] = oldRolesByNames[roleName]; + } + return rolesByNames; +} + +function deleteRoleInRolesByNames(roles: {}, roleName: string) { + let newRoles = {}; + for (let rolename in roles) { + if (rolename !== roleName) newRoles[rolename] = roles[rolename]; + } + return newRoles; +} + +function deleteRoleInEntries(roles: [], roleName: string) { + let newRoles = []; + for (let role of roles) { + if (role !== roleName) newRoles.push(role); + } + return newRoles; +} + +const reducerByName = (state: any, rolename: string, newRoleState: any) => { + const newRolesByNames = { + ...state, + [rolename]: newRoleState + }; + + return newRolesByNames; +}; + +function listReducer(state: any = {}, action: any = {}) { + switch (action.type) { + case FETCH_ROLES_SUCCESS: + const roles = action.payload._embedded.roles; + const roleNames = roles.map(role => role.name); + return { + ...state, + entries: roleNames, + entry: { + roleCreatePermission: action.payload._links.create ? true : false, + page: action.payload.page, + pageTotal: action.payload.pageTotal, + _links: action.payload._links + } + }; + + // Delete single role actions + case DELETE_ROLE_SUCCESS: + const newRoleEntries = deleteRoleInEntries( + state.entries, + action.payload.name + ); + return { + ...state, + entries: newRoleEntries + }; + default: + return state; + } +} + +function byNamesReducer(state: any = {}, action: any = {}) { + switch (action.type) { + // Fetch all roles actions + case FETCH_ROLES_SUCCESS: + const roles = action.payload._embedded.roles; + const roleNames = roles.map(role => role.name); + const byNames = extractRolesByNames(roles, roleNames, state.byNames); + return { + ...byNames + }; + + // Fetch single role actions + case FETCH_ROLE_SUCCESS: + return reducerByName(state, action.payload.name, action.payload); + + case DELETE_ROLE_SUCCESS: + return deleteRoleInRolesByNames( + state, + action.payload.name + ); + + default: + return state; + } +} + +export default combineReducers({ + list: listReducer, + byNames: byNamesReducer +}); + +// selectors +const selectList = (state: Object) => { + if (state.roles && state.roles.list) { + return state.roles.list; + } + return {}; +}; + +const selectListEntry = (state: Object): Object => { + const list = selectList(state); + if (list.entry) { + return list.entry; + } + return {}; +}; + +export const selectListAsCollection = (state: Object): PagedCollection => { + return selectListEntry(state); +}; + +export const isPermittedToCreateRoles = (state: Object): boolean => { + const permission = selectListEntry(state).roleCreatePermission; + if (permission) { + return true; + } + return false; +}; + +export function getRolesFromState(state: Object) { + const roleNames = selectList(state).entries; + if (!roleNames) { + return null; + } + const roleEntries: Role[] = []; + + for (let roleName of roleNames) { + roleEntries.push(state.roles.byNames[roleName]); + } + + return roleEntries; +} + +export function isFetchRolesPending(state: Object) { + return isPending(state, FETCH_ROLES); +} + +export function getFetchRolesFailure(state: Object) { + return getFailure(state, FETCH_ROLES); +} + +export function isCreateRolePending(state: Object) { + return isPending(state, CREATE_ROLE); +} + +export function getCreateRoleFailure(state: Object) { + return getFailure(state, CREATE_ROLE); +} + +export function getRoleByName(state: Object, name: string) { + if (state.roles && state.roles.byNames) { + return state.roles.byNames[name]; + } +} + +export function isFetchRolePending(state: Object, name: string) { + return isPending(state, FETCH_ROLE, name); +} + +export function getFetchRoleFailure(state: Object, name: string) { + return getFailure(state, FETCH_ROLE, name); +} + +export function isModifyRolePending(state: Object, name: string) { + return isPending(state, MODIFY_ROLE, name); +} + +export function getModifyRoleFailure(state: Object, name: string) { + return getFailure(state, MODIFY_ROLE, name); +} + +export function isDeleteRolePending(state: Object, name: string) { + return isPending(state, DELETE_ROLE, name); +} + +export function getDeleteRoleFailure(state: Object, name: string) { + return getFailure(state, DELETE_ROLE, name); +} From b51858c3fa3f7997a327314ce15a28fc86024930 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 7 May 2019 16:52:27 +0200 Subject: [PATCH 18/91] added Role ui-type --- scm-ui-components/packages/ui-types/src/Role.js | 9 +++++++++ scm-ui-components/packages/ui-types/src/index.js | 1 + 2 files changed, 10 insertions(+) create mode 100644 scm-ui-components/packages/ui-types/src/Role.js diff --git a/scm-ui-components/packages/ui-types/src/Role.js b/scm-ui-components/packages/ui-types/src/Role.js new file mode 100644 index 0000000000..385b498d38 --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/Role.js @@ -0,0 +1,9 @@ +//@flow + +export type Role = { + name: string, + creationDate: number | null, + lastModified: number | null, + type: string, + verb: string[] +}; diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index c57a3c6792..59c423c5d5 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -16,6 +16,7 @@ export type { Changeset } from "./Changesets"; export type { Tag } from "./Tags"; export type { Config } from "./Config"; +export type { Role } from "./Role"; export type { IndexResources } from "./IndexResources"; From d105ec784f86b1cf2604440d11eab4a2f3f43cd2 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 8 May 2019 09:32:51 +0200 Subject: [PATCH 19/91] added globalPermissionRoles --- scm-ui/src/config/containers/Config.js | 10 ++ .../containers/GlobalPermissionRoles.js | 130 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 scm-ui/src/config/containers/GlobalPermissionRoles.js diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 04de525c95..ee6a6d4f59 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -7,6 +7,7 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions"; import type { Links } from "@scm-manager/ui-types"; import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; import GlobalConfig from "./GlobalConfig"; +import GlobalPermissionRoles from "./GlobalPermissionRoles"; import type { History } from "history"; import { connect } from "react-redux"; import { compose } from "redux"; @@ -47,6 +48,11 @@ class Config extends React.Component {
+ { to={`${url}`} label={t("config.globalConfigurationNavLink")} /> + string, + history: History, + location: any, + + // dispatch functions + fetchRolesByPage: (link: string, page: number, filter?: string) => void +}; + +class GlobalPermissionRoles extends React.Component { + componentDidMount() { + const { fetchRolesByPage, rolesLink, page, location } = this.props; + fetchRolesByPage( + rolesLink, + page, + urls.getQueryStringFromLocation(location) + ); + } + + render() { + const { t, loading } = this.props; + + if (loading) { + return ; + } + + return ( +
+ + {this.renderPermissionsTable()} + {this.renderCreateButton()} + </div> + ); + } + + renderPermissionsTable() { + const { roles, list, page, location, t } = this.props; + if (roles && roles.length > 0) { + return ( + <> + <RoleTable roles={roles} /> + <LinkPaginator + collection={list} + page={page} + filter={urls.getQueryStringFromLocation(location)} + /> + </> + ); + } + return <Notification type="info">{t("config.roles.noPermissionRoles")}</Notification>; + } + + renderCreateButton() { + const { canAddRoles, t } = this.props; + if (canAddRoles) { + return ( + <CreateButton label={t("config.permissions.createButton")} link="/create" /> + ); + } + return null; + } +} + +const mapStateToProps = (state, ownProps) => { + const { match } = ownProps; + const roles = getRolesFromState(state); + const loading = isFetchRolesPending(state); + const error = getFetchRolesFailure(state); + const page = urls.getPageFromMatch(match); + const canAddRoles = isPermittedToCreateRoles(state); + const list = selectListAsCollection(state); + const rolesLink = getRolesLink(state); + + return { + roles, + loading, + error, + canAddRoles, + list, + page, + rolesLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchRolesByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchRolesByPage(link, page, filter)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("config")(GlobalPermissionRoles)); From 4f5cb613487cde9c27fc2eef39a4d5ea71d72199 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 8 May 2019 09:34:07 +0200 Subject: [PATCH 20/91] added temp indexResource + RoleTable --- .../src/config/components/table/RoleTable.js | 34 +++++++++++++++++++ scm-ui/src/modules/indexResource.js | 5 +++ 2 files changed, 39 insertions(+) create mode 100644 scm-ui/src/config/components/table/RoleTable.js diff --git a/scm-ui/src/config/components/table/RoleTable.js b/scm-ui/src/config/components/table/RoleTable.js new file mode 100644 index 0000000000..ed0c298b0e --- /dev/null +++ b/scm-ui/src/config/components/table/RoleTable.js @@ -0,0 +1,34 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Role } from "@scm-manager/ui-types"; + +type Props = { + t: string => string, + roles: Role[] +}; + +class RoleTable extends React.Component<Props> { + render() { + const { roles, t } = this.props; + return ( + <table className="card-table table is-hoverable is-fullwidth"> + <thead> + <tr> + <th className="is-hidden-mobile">{t("user.name")}</th> + <th>{t("user.displayName")}</th> + <th>{t("user.mail")}</th> + <th className="is-hidden-mobile">{t("user.active")}</th> + </tr> + </thead> + <tbody> + {roles.map((role, index) => { + return <p key={index}>role</p>; + })} + </tbody> + </table> + ); + } +} + +export default translate("config")(RoleTable); diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index df55c63756..909d356752 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -151,6 +151,11 @@ export function getSvnConfigLink(state: Object) { return getLink(state, "svnConfig"); } +export function getRolesLink(state: Object) { + //return getLink(state, "availableRepositoryPermissions"); + return "http://localhost:8081/scm/api/v2/repository-roles"; // TODO +} + export function getUserAutoCompleteLink(state: Object): string { const link = getLinkCollection(state, "autocomplete").find( i => i.name === "users" From 5eb074dde2d1ac7dfd752fcb0e3faf8c0a8f4328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 8 May 2019 09:34:57 +0200 Subject: [PATCH 21/91] Add index link for repository roles --- .../java/sonia/scm/api/v2/resources/IndexDtoGenerator.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index a3e7568957..40c04f3de7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -8,6 +8,7 @@ import org.apache.shiro.SecurityUtils; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.group.GroupPermissions; +import sonia.scm.repository.RepositoryRolePermissions; import sonia.scm.security.PermissionPermissions; import sonia.scm.user.UserPermissions; @@ -62,6 +63,9 @@ public class IndexDtoGenerator extends HalAppenderMapper { builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self())); builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self())); + if (RepositoryRolePermissions.read().isPermitted()) { + builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self())); + } } else { builder.single(link("login", resourceLinks.authentication().jsonLogin())); } From dd312308fae451d6e16698c37c29bf60b9ae3fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 8 May 2019 10:13:01 +0200 Subject: [PATCH 22/91] Add system roles to repository role REST resource --- .../api/v2/resources/RepositoryRoleDto.java | 1 + ...positoryRoleToRepositoryRoleDtoMapper.java | 14 ++-- .../DefaultRepositoryRoleManager.java | 18 +++-- .../RepositoryRoleRootResourceTest.java | 69 +++++++++++++------ 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java index 50867b4f92..b81789c110 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java @@ -18,6 +18,7 @@ public class RepositoryRoleDto extends HalRepresentation { private String name; @NoBlankStrings @NotEmpty private Collection<String> verbs; + private boolean system; RepositoryRoleDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java index 86b4709fd2..5f77e38fed 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java @@ -3,9 +3,9 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.ObjectFactory; import sonia.scm.repository.RepositoryRole; -import sonia.scm.repository.RepositoryRoleManager; import sonia.scm.repository.RepositoryRolePermissions; import javax.inject.Inject; @@ -19,16 +19,17 @@ import static de.otto.edison.hal.Links.linkingTo; @Mapper public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper<RepositoryRole, RepositoryRoleDto> { - @Inject - private RepositoryRoleManager repositoryRoleManager; - @Inject private ResourceLinks resourceLinks; + @Override + @Mapping(source = "type", target = "system") + public abstract RepositoryRoleDto map(RepositoryRole modelObject); + @ObjectFactory RepositoryRoleDto createDto(RepositoryRole repositoryRole) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName())); - if (RepositoryRolePermissions.modify().isPermitted()) { + if (!isSystemRole(repositoryRole.getType()) && RepositoryRolePermissions.modify().isPermitted()) { linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName()))); linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName()))); } @@ -39,4 +40,7 @@ public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper return new RepositoryRoleDto(linksBuilder.build(), embeddedBuilder.build()); } + boolean isSystemRole(String type) { + return "system".equals(type); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java index bc52d38dd2..2507d4bf25 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java @@ -33,8 +33,6 @@ package sonia.scm.repository; -import com.github.sdorra.ssp.PermissionActionCheck; -import com.github.sdorra.ssp.PermissionCheck; import com.google.inject.Inject; import com.google.inject.Singleton; import org.slf4j.Logger; @@ -44,6 +42,7 @@ import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; import sonia.scm.NotFoundException; import sonia.scm.SCMContextProvider; +import sonia.scm.security.RepositoryPermissionProvider; import sonia.scm.util.Util; import java.util.ArrayList; @@ -51,6 +50,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; @Singleton @EagerSingleton @@ -62,10 +62,11 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager LoggerFactory.getLogger(DefaultRepositoryRoleManager.class); @Inject - public DefaultRepositoryRoleManager(RepositoryRoleDAO repositoryRoleDAO) + public DefaultRepositoryRoleManager(RepositoryRoleDAO repositoryRoleDAO, RepositoryPermissionProvider repositoryPermissionProvider) { this.repositoryRoleDAO = repositoryRoleDAO; this.managerDaoAdapter = new ManagerDaoAdapter<>(repositoryRoleDAO); + this.repositoryPermissionProvider = repositoryPermissionProvider; } @Override @@ -131,6 +132,10 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager public RepositoryRole get(String id) { RepositoryRolePermissions.read(); + return findSystemRole(id).orElse(findCustomRole(id)); + } + + private RepositoryRole findCustomRole(String id) { RepositoryRole repositoryRole = repositoryRoleDAO.get(id); if (repositoryRole != null) { @@ -140,6 +145,10 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager } } + private Optional<RepositoryRole> findSystemRole(String id) { + return repositoryPermissionProvider.availableRoles().stream().filter(role -> role.getName().equals(id)).findFirst(); + } + @Override public Collection<RepositoryRole> getAll() { return getAll(repositoryRole -> true, null); @@ -152,7 +161,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager if (!RepositoryRolePermissions.read().isPermitted()) { return Collections.emptySet(); } - for (RepositoryRole repositoryRole : repositoryRoleDAO.getAll()) { + for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) { repositoryRoles.add(repositoryRole.clone()); } @@ -188,4 +197,5 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager private final RepositoryRoleDAO repositoryRoleDAO; private final ManagerDaoAdapter<RepositoryRole> managerDaoAdapter; + private final RepositoryPermissionProvider repositoryPermissionProvider; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java index 0323df27e4..0489717926 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java @@ -28,7 +28,7 @@ import java.net.URISyntaxException; import java.util.Collections; import static java.net.URI.create; -import static java.util.Collections.singletonList; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -46,8 +46,10 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @RunWith(MockitoJUnitRunner.Silent.class) public class RepositoryRoleRootResourceTest { - public static final String EXISTING_ROLE = "existingRole"; - public static final RepositoryRole REPOSITORY_ROLE = new RepositoryRole("existingRole", Collections.singleton("verb"), "xml"); + public static final String CUSTOM_ROLE = "customRole"; + public static final String SYSTEM_ROLE = "systemRole"; + public static final RepositoryRole CUSTOM_REPOSITORY_ROLE = new RepositoryRole(CUSTOM_ROLE, Collections.singleton("verb"), "xml"); + public static final RepositoryRole SYSTEM_REPOSITORY_ROLE = new RepositoryRole(SYSTEM_ROLE, Collections.singleton("admin"), "system"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/")); @Rule @@ -88,8 +90,9 @@ public class RepositoryRoleRootResourceTest { dispatcher = createDispatcher(rootResource); dispatcher.getProviderFactory().registerProviderInstance(new JSONContextResolver(new ObjectMapperProvider().get())); - when(repositoryRoleManager.get(EXISTING_ROLE)).thenReturn(REPOSITORY_ROLE); - when(repositoryRoleManager.getPage(any(), any(), anyInt(), anyInt())).thenReturn(new PageResult<>(singletonList(REPOSITORY_ROLE), 1)); + when(repositoryRoleManager.get(CUSTOM_ROLE)).thenReturn(CUSTOM_REPOSITORY_ROLE); + when(repositoryRoleManager.get(SYSTEM_ROLE)).thenReturn(SYSTEM_REPOSITORY_ROLE); + when(repositoryRoleManager.getPage(any(), any(), anyInt(), anyInt())).thenReturn(new PageResult<>(asList(CUSTOM_REPOSITORY_ROLE, SYSTEM_REPOSITORY_ROLE), 2)); } @Test @@ -103,8 +106,8 @@ public class RepositoryRoleRootResourceTest { } @Test - public void shouldGetExistingRole() throws URISyntaxException, UnsupportedEncodingException { - MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole"); + public void shouldGetCustomRole() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -112,17 +115,38 @@ public class RepositoryRoleRootResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()) .contains( - "\"name\":\"existingRole\"", + "\"name\":\"" + CUSTOM_ROLE + "\"", "\"verbs\":[\"verb\"]", - "\"self\":{\"href\":\"/v2/repository-roles/existingRole\"}", - "\"delete\":{\"href\":\"/v2/repository-roles/existingRole\"}" + "\"self\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", + "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" + ); + } + + @Test + public void shouldGetSystemRole() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + SYSTEM_ROLE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()) + .contains( + "\"name\":\"" + SYSTEM_ROLE + "\"", + "\"verbs\":[\"admin\"]", + "\"self\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + ) + .doesNotContain( + "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" ); } @Test @SubjectAware(username = "dent") public void shouldNotGetDeleteLinkWithoutPermission() throws URISyntaxException, UnsupportedEncodingException { - MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole"); + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -135,23 +159,23 @@ public class RepositoryRoleRootResourceTest { @Test public void shouldUpdateRole() throws URISyntaxException { MockHttpRequest request = MockHttpRequest - .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole") + .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE) .contentType(VndMediaType.REPOSITORY_ROLE) - .content(content("{'name': 'existingRole', 'verbs': ['write', 'push']}")); + .content(content("{'name': '" + CUSTOM_ROLE + "', 'verbs': ['write', 'push']}")); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); verify(repositoryRoleManager).modify(any()); - assertThat(modifyCaptor.getValue().getName()).isEqualTo("existingRole"); + assertThat(modifyCaptor.getValue().getName()).isEqualTo(CUSTOM_ROLE); assertThat(modifyCaptor.getValue().getVerbs()).containsExactly("write", "push"); } @Test public void shouldNotChangeRoleName() throws URISyntaxException { MockHttpRequest request = MockHttpRequest - .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole") + .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE) .contentType(VndMediaType.REPOSITORY_ROLE) .content(content("{'name': 'changedName', 'verbs': ['write', 'push']}")); MockHttpResponse response = new MockHttpResponse(); @@ -197,14 +221,14 @@ public class RepositoryRoleRootResourceTest { @Test public void shouldDeleteRole() throws URISyntaxException { MockHttpRequest request = MockHttpRequest - .delete("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + EXISTING_ROLE); + .delete("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); verify(repositoryRoleManager).delete(any()); - assertThat(deleteCaptor.getValue().getName()).isEqualTo(EXISTING_ROLE); + assertThat(deleteCaptor.getValue().getName()).isEqualTo(CUSTOM_ROLE); } @Test @@ -217,12 +241,17 @@ public class RepositoryRoleRootResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()) .contains( - "\"name\":\"existingRole\"", + "\"name\":\"" + CUSTOM_ROLE + "\"", + "\"name\":\"" + SYSTEM_ROLE + "\"", "\"verbs\":[\"verb\"]", + "\"verbs\":[\"admin\"]", "\"self\":{\"href\":\"/v2/repository-roles", - "\"delete\":{\"href\":\"/v2/repository-roles/existingRole\"}", + "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", "\"create\":{\"href\":\"/v2/repository-roles/\"}" - ); + ) + .doesNotContain( + "\"delete\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + ); } @Test From c88654739bc7a4ef2a4707eb8e7fa4f128895141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 8 May 2019 10:55:24 +0200 Subject: [PATCH 23/91] System roles should not be modifiable --- .../DefaultRepositoryRoleManager.java | 12 ++- .../DefaultRepositoryRoleManagerTest.java | 87 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java index 2507d4bf25..82b3f8bd58 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java @@ -35,6 +35,7 @@ package sonia.scm.repository; import com.google.inject.Inject; import com.google.inject.Singleton; +import org.apache.shiro.authz.UnauthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.EagerSingleton; @@ -76,6 +77,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager @Override public RepositoryRole create(RepositoryRole repositoryRole) { + assertNoSystemRole(repositoryRole); String type = repositoryRole.getType(); if (Util.isEmpty(type)) { repositoryRole.setType(repositoryRoleDAO.getType()); @@ -93,6 +95,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager @Override public void delete(RepositoryRole repositoryRole) { + assertNoSystemRole(repositoryRole); logger.info("delete repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType()); managerDaoAdapter.delete( repositoryRole, @@ -108,6 +111,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager @Override public void modify(RepositoryRole repositoryRole) { + assertNoSystemRole(repositoryRole); logger.info("modify repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType()); managerDaoAdapter.modify( repositoryRole, @@ -130,11 +134,17 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager @Override public RepositoryRole get(String id) { - RepositoryRolePermissions.read(); + RepositoryRolePermissions.read().check(); return findSystemRole(id).orElse(findCustomRole(id)); } + private void assertNoSystemRole(RepositoryRole repositoryRole) { + if (findSystemRole(repositoryRole.getId()).isPresent()) { + throw new UnauthorizedException("system roles cannot be modified"); + } + } + private RepositoryRole findCustomRole(String id) { RepositoryRole repositoryRole = repositoryRoleDAO.get(id); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java new file mode 100644 index 0000000000..3f2d62c600 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java @@ -0,0 +1,87 @@ +package sonia.scm.repository; + +import org.apache.shiro.authz.UnauthorizedException; +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.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.security.RepositoryPermissionProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultRepositoryRoleManagerTest { + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + @Mock + private RepositoryRoleDAO dao; + @Mock + private RepositoryPermissionProvider permissionProvider; + + @InjectMocks + private DefaultRepositoryRoleManager manager; + + @BeforeEach + void initUser() { + subjectThreadState.bind(); + doAnswer(invocation -> { + String permission = invocation.getArguments()[0].toString(); + if (!subject.isPermitted(permission)) { + throw new UnauthorizedException(permission); + } + return null; + }).when(subject).checkPermission(anyString()); + ThreadContext.bind(subject); + } + + @AfterEach + void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Nested + class WithAuthorizedUser { + + @BeforeEach + void authorizeUser() { + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @Test + void shouldReturnNull_forNotExistingRole() { + RepositoryRole role = manager.get("noSuchRole"); + assertThat(role).isNull(); + } + } + + @Nested + class WithUnauthorizedUser { + + @BeforeEach + void authorizeUser() { + when(subject.isPermitted(any(String.class))).thenReturn(false); + } + + @Test + void x() { + assertThrows(UnauthorizedException.class, () -> manager.get("noSuchRole")); + } + } +} From 6f1f74b208670a1c260e9a8abacda7395b3db3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 8 May 2019 12:36:22 +0200 Subject: [PATCH 24/91] Fix repository role manager --- .../DefaultRepositoryRoleManager.java | 38 ++--- .../DefaultRepositoryRoleManagerTest.java | 134 +++++++++++++++++- 2 files changed, 151 insertions(+), 21 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java index 82b3f8bd58..ab0ad16d0e 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java @@ -53,6 +53,7 @@ import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Predicate; +import java.util.stream.Collectors; @Singleton @EagerSingleton public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager @@ -156,38 +157,43 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager } private Optional<RepositoryRole> findSystemRole(String id) { - return repositoryPermissionProvider.availableRoles().stream().filter(role -> role.getName().equals(id)).findFirst(); + return repositoryPermissionProvider + .availableRoles() + .stream() + .filter(role -> !repositoryRoleDAO.getType().equals(role.getType())) + .filter(role -> role.getName().equals(id)).findFirst(); } @Override - public Collection<RepositoryRole> getAll() { - return getAll(repositoryRole -> true, null); - } - - @Override - public Collection<RepositoryRole> getAll(Predicate<RepositoryRole> filter, Comparator<RepositoryRole> comparator) { + public List<RepositoryRole> getAll() { List<RepositoryRole> repositoryRoles = new ArrayList<>(); if (!RepositoryRolePermissions.read().isPermitted()) { - return Collections.emptySet(); + return Collections.emptyList(); } for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) { repositoryRoles.add(repositoryRole.clone()); } - if (comparator != null) { - Collections.sort(repositoryRoles, comparator); - } - return repositoryRoles; } @Override - public Collection<RepositoryRole> getAll(Comparator<RepositoryRole> comaparator, int start, int limit) { - if (!RepositoryRolePermissions.read().isPermitted()) { - return Collections.emptySet(); + public Collection<RepositoryRole> getAll(Predicate<RepositoryRole> filter, Comparator<RepositoryRole> comparator) { + List<RepositoryRole> repositoryRoles = getAll(); + + List<RepositoryRole> filteredRoles = repositoryRoles.stream().filter(filter::test).collect(Collectors.toList()); + + if (comparator != null) { + filteredRoles.sort(comparator); } - return Util.createSubCollection(repositoryRoleDAO.getAll(), comaparator, + + return filteredRoles; + } + + @Override + public Collection<RepositoryRole> getAll(Comparator<RepositoryRole> comaparator, int start, int limit) { + return Util.createSubCollection(getAll(), comaparator, (collection, item) -> { collection.add(item.clone()); }, start, limit); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java index 3f2d62c600..0542626dd5 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java @@ -5,7 +5,6 @@ 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.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -14,19 +13,36 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import sonia.scm.NotFoundException; import sonia.scm.security.RepositoryPermissionProvider; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class DefaultRepositoryRoleManagerTest { + private static final String CUSTOM_ROLE_NAME = "customRole"; + private static final String SYSTEM_ROLE_NAME = "systemRole"; + private static final RepositoryRole CUSTOM_ROLE = new RepositoryRole(CUSTOM_ROLE_NAME, singletonList("custom"), "xml"); + private static final RepositoryRole SYSTEM_ROLE = new RepositoryRole(SYSTEM_ROLE_NAME, singletonList("system"), "system"); + private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -51,6 +67,17 @@ class DefaultRepositoryRoleManagerTest { ThreadContext.bind(subject); } + @BeforeEach + void initDao() { + when(dao.getType()).thenReturn("xml"); + } + + @BeforeEach + void mockExistingRole() { + when(dao.get(CUSTOM_ROLE_NAME)).thenReturn(CUSTOM_ROLE); + when(permissionProvider.availableRoles()).thenReturn(asList(CUSTOM_ROLE, SYSTEM_ROLE)); + } + @AfterEach void cleanupContext() { ThreadContext.unbindSubject(); @@ -61,7 +88,8 @@ class DefaultRepositoryRoleManagerTest { @BeforeEach void authorizeUser() { - when(subject.isPermitted(any(String.class))).thenReturn(true); + when(subject.isPermitted("repositoryRole:read")).thenReturn(true); + when(subject.isPermitted("repositoryRole:modify")).thenReturn(true); } @Test @@ -69,6 +97,75 @@ class DefaultRepositoryRoleManagerTest { RepositoryRole role = manager.get("noSuchRole"); assertThat(role).isNull(); } + + @Test + void shouldReturnRole_forExistingRole() { + RepositoryRole role = manager.get(CUSTOM_ROLE_NAME); + assertThat(role).isNotNull(); + } + + @Test + void shouldCreateRole() { + RepositoryRole role = manager.create(new RepositoryRole("new", singletonList("custom"), null)); + assertThat(role.getType()).isEqualTo("xml"); + verify(dao).add(role); + } + + @Test + void shouldNotCreateRole_whenSystemRoleExists() { + assertThrows(UnauthorizedException.class, () -> manager.create(new RepositoryRole(SYSTEM_ROLE_NAME, singletonList("custom"), null))); + verify(dao, never()).add(any()); + } + + @Test + void shouldModifyRole() { + RepositoryRole role = new RepositoryRole(CUSTOM_ROLE_NAME, singletonList("changed"), null); + manager.modify(role); + verify(dao).modify(role); + } + + @Test + void shouldNotModifyRole_whenRoleDoesNotExists() { + assertThrows(NotFoundException.class, () -> manager.modify(new RepositoryRole("noSuchRole", singletonList("changed"), null))); + verify(dao, never()).modify(any()); + } + + @Test + void shouldNotModifyRole_whenSystemRoleExists() { + assertThrows(UnauthorizedException.class, () -> manager.modify(new RepositoryRole(SYSTEM_ROLE_NAME, singletonList("changed"), null))); + verify(dao, never()).modify(any()); + } + + @Test + void shouldReturnAllRoles() { + List<RepositoryRole> allRoles = manager.getAll(); + assertThat(allRoles).containsExactly(CUSTOM_ROLE, SYSTEM_ROLE); + } + + @Test + void shouldReturnFilteredRoles() { + Collection<RepositoryRole> allRoles = manager.getAll(role -> CUSTOM_ROLE_NAME.equals(role.getName()), null); + assertThat(allRoles).containsExactly(CUSTOM_ROLE); + } + + @Test + void shouldReturnOrderedFilteredRoles() { + Collection<RepositoryRole> allRoles = + manager.getAll( + role -> true, + Comparator.comparing(RepositoryRole::getType)); + assertThat(allRoles).containsExactly(SYSTEM_ROLE, CUSTOM_ROLE); + } + + @Test + void shouldReturnPaginatedRoles() { + Collection<RepositoryRole> allRoles = + manager.getAll( + Comparator.comparing(RepositoryRole::getType), + 1, 1 + ); + assertThat(allRoles).containsExactly(CUSTOM_ROLE); + } } @Nested @@ -80,8 +177,35 @@ class DefaultRepositoryRoleManagerTest { } @Test - void x() { - assertThrows(UnauthorizedException.class, () -> manager.get("noSuchRole")); + void shouldThrowException_forGet() { + assertThrows(UnauthorizedException.class, () -> manager.get("any")); + } + + @Test + void shouldThrowException_forCreate() { + assertThrows(UnauthorizedException.class, () -> manager.create(new RepositoryRole("new", singletonList("custom"), null))); + verify(dao, never()).add(any()); + } + + @Test + void shouldThrowException_forModify() { + assertThrows(UnauthorizedException.class, () -> manager.modify(new RepositoryRole(CUSTOM_ROLE_NAME, singletonList("custom"), null))); + verify(dao, never()).modify(any()); + } + + @Test + void shouldReturnEmptyList() { + assertThat(manager.getAll()).isEmpty(); + } + + @Test + void shouldReturnEmptyFilteredList() { + assertThat(manager.getAll(x -> true, null)).isEmpty(); + } + + @Test + void shouldReturnEmptyPaginatedList() { + assertThat(manager.getAll(1, 1)).isEmpty(); } } } From 61844b917dffa75923d79c2886f2c05e04935a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 8 May 2019 12:36:41 +0200 Subject: [PATCH 25/91] Use camelCase for url --- .../sonia/scm/api/v2/resources/RepositoryRoleRootResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java index f4adb02438..44e3a10fba 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleRootResource.java @@ -10,7 +10,7 @@ import javax.ws.rs.Path; @Path(RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2) public class RepositoryRoleRootResource { - static final String REPOSITORY_ROLES_PATH_V2 = "v2/repository-roles/"; + static final String REPOSITORY_ROLES_PATH_V2 = "v2/repositoryRoles/"; private final Provider<RepositoryRoleCollectionResource> repositoryRoleCollectionResource; private final Provider<RepositoryRoleResource> repositoryRoleResource; From 66b818780456825a3226cb1db8149d95ffbd5bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 8 May 2019 12:54:32 +0200 Subject: [PATCH 26/91] Fix integration test --- scm-it/src/test/java/sonia/scm/it/RoleITCase.java | 12 +++++++++--- .../test/java/sonia/scm/it/utils/ScmRequests.java | 7 ++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/scm-it/src/test/java/sonia/scm/it/RoleITCase.java b/scm-it/src/test/java/sonia/scm/it/RoleITCase.java index 998c4e315e..331a251d23 100644 --- a/scm-it/src/test/java/sonia/scm/it/RoleITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RoleITCase.java @@ -3,12 +3,14 @@ package sonia.scm.it; import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.Test; -import sonia.scm.it.utils.RestUtil; +import sonia.scm.it.utils.ScmRequests; import sonia.scm.it.utils.TestData; import sonia.scm.web.VndMediaType; import static org.junit.Assert.assertNotNull; import static sonia.scm.it.PermissionsITCase.USER_PASS; +import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD; +import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME; import static sonia.scm.it.utils.RestUtil.given; import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; import static sonia.scm.it.utils.TestData.callRepository; @@ -28,9 +30,13 @@ public class RoleITCase { public void userShouldSeePermissionsAfterAddingRoleToUser() { callRepository(USER, USER_PASS, "git", HttpStatus.SC_FORBIDDEN); + String repositoryRolesUrl = new ScmRequests() + .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD) + .getUrl("repositoryRoles"); + given() .when() - .delete(RestUtil.createResourceUrl("repository-roles/" + ROLE_NAME)) + .delete(repositoryRolesUrl + ROLE_NAME) .then() .statusCode(HttpStatus.SC_NO_CONTENT); @@ -40,7 +46,7 @@ public class RoleITCase { "\"name\": \"" + ROLE_NAME + "\"," + "\"verbs\": [\"read\",\"permissionRead\"]" + "}") - .post(RestUtil.createResourceUrl("repository-roles/")) + .post(repositoryRolesUrl) .then() .statusCode(HttpStatus.SC_CREATED); diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java index 2164617772..69b9940f70 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -201,7 +201,12 @@ public class ScmRequests { return super.assertPropertyPathDoesNotExists(LINK_USERS); } - + public String getUrl(String linkName) { + return response + .then() + .extract() + .path("_links." + linkName + ".href"); + } } public class RepositoryResponse<PREV extends ModelResponse> extends ModelResponse<RepositoryResponse<PREV>, PREV> { From b86d17914cfab5bc2c828cc812ae14ae55869f15 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 8 May 2019 13:13:50 +0200 Subject: [PATCH 27/91] added trans for permissionRoles --- scm-ui/public/locales/de/config.json | 13 +++++++++++++ scm-ui/public/locales/en/config.json | 13 +++++++++++++ .../table/{RoleTable.js => PermissionRoleTable.js} | 10 ++++------ scm-ui/src/config/containers/Config.js | 2 +- .../src/config/containers/GlobalPermissionRoles.js | 10 +++++----- 5 files changed, 36 insertions(+), 12 deletions(-) rename scm-ui/src/config/components/table/{RoleTable.js => PermissionRoleTable.js} (63%) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index b255e1c233..b3d845acc8 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -6,6 +6,19 @@ "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Einstellungen Fehler" }, + "roles": { + "navLink": "Berechtigungsrollen", + "title": "Berechtigungsrollen", + "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", + "createButton": "Berechtigungsrolle erstellen", + "form": { + "name": "Name", + "system": "System", + "permissions": "Berechtigungen", + "subtitle": "Berechtigungsrolle bearbeiten", + "submit": "Speichern" + } + }, "config-form": { "submit": "Speichern", "submit-success-notification": "Einstellungen wurden erfolgreich geändert!", diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index aeb62c1847..404dfb7e14 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -6,6 +6,19 @@ "errorTitle": "Error", "errorSubtitle": "Unknown Config Error" }, + "roles": { + "navLink": "Permission Roles", + "title": "Permission Roles", + "noPermissionRoles": "No permission roles found.", + "createButton": "Create Permission Role", + "form": { + "name": "Name", + "system": "System", + "permissions": "Permissions", + "subtitle": "Edit Permission Roles", + "submit": "Save" + } + }, "config-form": { "submit": "Submit", "submit-success-notification": "Configuration changed successfully!", diff --git a/scm-ui/src/config/components/table/RoleTable.js b/scm-ui/src/config/components/table/PermissionRoleTable.js similarity index 63% rename from scm-ui/src/config/components/table/RoleTable.js rename to scm-ui/src/config/components/table/PermissionRoleTable.js index ed0c298b0e..aad77a5654 100644 --- a/scm-ui/src/config/components/table/RoleTable.js +++ b/scm-ui/src/config/components/table/PermissionRoleTable.js @@ -8,17 +8,15 @@ type Props = { roles: Role[] }; -class RoleTable extends React.Component<Props> { +class PermissionRoleTable extends React.Component<Props> { render() { const { roles, t } = this.props; return ( <table className="card-table table is-hoverable is-fullwidth"> <thead> <tr> - <th className="is-hidden-mobile">{t("user.name")}</th> - <th>{t("user.displayName")}</th> - <th>{t("user.mail")}</th> - <th className="is-hidden-mobile">{t("user.active")}</th> + <th>{t("role.form.name")}</th> + <th>{t("role.form.permissions")}</th> </tr> </thead> <tbody> @@ -31,4 +29,4 @@ class RoleTable extends React.Component<Props> { } } -export default translate("config")(RoleTable); +export default translate("config")(PermissionRoleTable); diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index ee6a6d4f59..388b9650d7 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -68,7 +68,7 @@ class Config extends React.Component<Props> { /> <NavLink to={`${url}/roles`} - label={t("config.roles.navLink")} + label={t("roles.navLink")} /> <ExtensionPoint name="config.navigation" diff --git a/scm-ui/src/config/containers/GlobalPermissionRoles.js b/scm-ui/src/config/containers/GlobalPermissionRoles.js index 6fd41d3e8a..63c8385daa 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoles.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoles.js @@ -20,7 +20,7 @@ import { urls, CreateButton } from "@scm-manager/ui-components"; -import RoleTable from "../components/table/RoleTable"; +import PermissionRoleTable from "../components/table/PermissionRoleTable"; import { getRolesLink } from "../../modules/indexResource"; type Props = { @@ -60,7 +60,7 @@ class GlobalPermissionRoles extends React.Component<Props> { return ( <div> - <Title title={t("config.roles.title")} /> + <Title title={t("roles.title")} /> {this.renderPermissionsTable()} {this.renderCreateButton()} </div> @@ -72,7 +72,7 @@ class GlobalPermissionRoles extends React.Component<Props> { if (roles && roles.length > 0) { return ( <> - <RoleTable roles={roles} /> + <PermissionRoleTable roles={roles} /> <LinkPaginator collection={list} page={page} @@ -81,14 +81,14 @@ class GlobalPermissionRoles extends React.Component<Props> { </> ); } - return <Notification type="info">{t("config.roles.noPermissionRoles")}</Notification>; + return <Notification type="info">{t("roles.noPermissionRoles")}</Notification>; } renderCreateButton() { const { canAddRoles, t } = this.props; if (canAddRoles) { return ( - <CreateButton label={t("config.permissions.createButton")} link="/create" /> + <CreateButton label={t("roles.createButton")} link="/create" /> ); } return null; From 1981e9fb034555940deb14f50acbfd76f724971e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 8 May 2019 13:14:39 +0200 Subject: [PATCH 28/91] Add permission for repository roles --- scm-webapp/src/main/resources/META-INF/scm/permissions.xml | 3 +++ scm-webapp/src/main/resources/locales/de/plugins.json | 6 ++++++ scm-webapp/src/main/resources/locales/en/plugins.json | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 27f343bc30..620fc484b1 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -66,5 +66,8 @@ <permission> <value>configuration:read,write:*</value> </permission> + <permission> + <value>repositoryRole:read,write</value> + </permission> </permissions> diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 41bb53de1e..0bdfa39b3e 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -60,6 +60,12 @@ } } }, + "repositoryRole": { + "read,write": { + "displayName": "Benutzerdefinierte Repository-Rollen-Berechtigungen verwalten", + "description": "Kann benutzerdefinierte Rollen und deren Berechtigungen erstellen, ändern und löschen" + } + }, "unknown": "Unbekannte Berechtigung" }, "verbs": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 0ad62ddf5c..4255f519ca 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -60,6 +60,12 @@ } } }, + "repositoryRole": { + "read,write": { + "displayName": "Administer custom repository role permissions", + "description": "May create, modify and delete custom repository roles and their permissions" + } + }, "unknown": "Unknown permission" }, "verbs": { From af3fdae49065734fb7caa80b46fc7a74150652f3 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 8 May 2019 13:17:06 +0200 Subject: [PATCH 29/91] adjusted modules to new permissionRoles api structure --- .../packages/ui-types/src/Role.js | 11 +- scm-ui/src/config/modules/roles.js | 33 ++- scm-ui/src/config/modules/roles.test.js | 188 ++++++++++++------ 3 files changed, 146 insertions(+), 86 deletions(-) diff --git a/scm-ui-components/packages/ui-types/src/Role.js b/scm-ui-components/packages/ui-types/src/Role.js index 385b498d38..00e794b886 100644 --- a/scm-ui-components/packages/ui-types/src/Role.js +++ b/scm-ui-components/packages/ui-types/src/Role.js @@ -1,9 +1,12 @@ //@flow +import type { Links } from "./hal"; + export type Role = { name: string, - creationDate: number | null, - lastModified: number | null, - type: string, - verb: string[] + verbs: string[], + creationDate?: number, + lastModified?: number, + system: boolean, + _links: Links }; diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index 823fbb7aa7..af81190bb4 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -4,7 +4,7 @@ import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; import * as types from "../../modules/types"; import { combineReducers, Dispatch } from "redux"; -import type {Action, PagedCollection, Role} from "@scm-manager/ui-types"; +import type { Action, PagedCollection, Role } from "@scm-manager/ui-types"; export const FETCH_ROLES = "scm/roles/FETCH_ROLES"; export const FETCH_ROLES_PENDING = `${FETCH_ROLES}_${types.PENDING_SUFFIX}`; @@ -318,24 +318,22 @@ function deleteRoleInEntries(roles: [], roleName: string) { } const reducerByName = (state: any, rolename: string, newRoleState: any) => { - const newRolesByNames = { + return { ...state, [rolename]: newRoleState }; - - return newRolesByNames; }; function listReducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_ROLES_SUCCESS: - const roles = action.payload._embedded.roles; + const roles = action.payload._embedded.repositoryRoles; const roleNames = roles.map(role => role.name); return { ...state, entries: roleNames, entry: { - roleCreatePermission: action.payload._links.create ? true : false, + roleCreatePermission: !!action.payload._links.create, page: action.payload.page, pageTotal: action.payload.pageTotal, _links: action.payload._links @@ -361,7 +359,7 @@ function byNamesReducer(state: any = {}, action: any = {}) { switch (action.type) { // Fetch all roles actions case FETCH_ROLES_SUCCESS: - const roles = action.payload._embedded.roles; + const roles = action.payload._embedded.repositoryRoles; const roleNames = roles.map(role => role.name); const byNames = extractRolesByNames(roles, roleNames, state.byNames); return { @@ -373,10 +371,7 @@ function byNamesReducer(state: any = {}, action: any = {}) { return reducerByName(state, action.payload.name, action.payload); case DELETE_ROLE_SUCCESS: - return deleteRoleInRolesByNames( - state, - action.payload.name - ); + return deleteRoleInRolesByNames(state, action.payload.name); default: return state; @@ -390,8 +385,8 @@ export default combineReducers({ // selectors const selectList = (state: Object) => { - if (state.roles && state.roles.list) { - return state.roles.list; + if (state.repositoryRoles && state.repositoryRoles.list) { + return state.repositoryRoles.list; } return {}; }; @@ -409,11 +404,7 @@ export const selectListAsCollection = (state: Object): PagedCollection => { }; export const isPermittedToCreateRoles = (state: Object): boolean => { - const permission = selectListEntry(state).roleCreatePermission; - if (permission) { - return true; - } - return false; + return selectListEntry(state).roleCreatePermission; }; export function getRolesFromState(state: Object) { @@ -424,7 +415,7 @@ export function getRolesFromState(state: Object) { const roleEntries: Role[] = []; for (let roleName of roleNames) { - roleEntries.push(state.roles.byNames[roleName]); + roleEntries.push(state.repositoryRoles.byNames[roleName]); } return roleEntries; @@ -447,8 +438,8 @@ export function getCreateRoleFailure(state: Object) { } export function getRoleByName(state: Object, name: string) { - if (state.roles && state.roles.byNames) { - return state.roles.byNames[name]; + if (state.repositoryRoles && state.repositoryRoles.byNames) { + return state.repositoryRoles.byNames[name]; } } diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/modules/roles.test.js index 7bb6bdcc60..2d2e30012c 100644 --- a/scm-ui/src/config/modules/roles.test.js +++ b/scm-ui/src/config/modules/roles.test.js @@ -49,11 +49,6 @@ import reducer, { isPermittedToCreateRoles } from "./roles"; -const URL = "roles"; -const ROLES_URL = "api/v2/repositoryPermissions"; - -const error = new Error("FEHLER!"); - const verbs = [ "createPullRequest", "readPullRequest", @@ -72,49 +67,64 @@ const verbs = [ "*" ]; -const repositoryRoles = { - availableVerbs: verbs, - availableRoles: [ - { - name: "READ", - creationDate: null, - lastModified: null, - type: "system", - verb: ["read", "pull", "readPullRequest"] - }, - { - name: "WRITE", - creationDate: null, - lastModified: null, - type: "system", - verb: [ - "read", - "pull", - "push", - "createPullRequest", - "readPullRequest", - "commentPullRequest", - "mergePullRequest" - ] - }, - { - name: "OWNER", - creationDate: null, - lastModified: null, - type: "system", - verb: ["*"] - } - ], +const role1 = { + name: "SPECIALROLE", + verbs: ["read", "pull", "push", "readPullRequest"], + system: false, _links: { self: { - href: "http://localhost:8081/scm/api/v2/repositoryPermissions/" + href: "http://localhost:8081/scm/api/v2/repositoryRoles/SPECIALROLE" + }, + delete: { + href: "http://localhost:8081/scm/api/v2/repositoryRoles/SPECIALROLE" + }, + update: { + href: "http://localhost:8081/scm/api/v2/repositoryRoles/SPECIALROLE" + } + } +}; +const role2 = { + name: "WRITE", + verbs: [ + "read", + "pull", + "push", + "createPullRequest", + "readPullRequest", + "commentPullRequest", + "mergePullRequest" + ], + system: true, + _links: { + self: { + href: "http://localhost:8081/scm/api/v2/repositoryRoles/WRITE" } } }; const responseBody = { - entries: repositoryRoles, - rolesUpdatePermission: false + page: 0, + pageTotal: 1, + _links: { + self: { + href: + "http://localhost:8081/scm/api/v2/repositoryRoles/?page=0&pageSize=10" + }, + first: { + href: + "http://localhost:8081/scm/api/v2/repositoryRoles/?page=0&pageSize=10" + }, + last: { + href: + "http://localhost:8081/scm/api/v2/repositoryRoles/?page=0&pageSize=10" + }, + create: { + href: "http://localhost:8081/scm/api/v2/repositoryRoles/" + } + }, + _embedded: { + repositoryRoles: [role1, role2] + } }; const response = { @@ -122,6 +132,12 @@ const response = { responseBody }; +const URL = "repositoryRoles"; +const ROLES_URL = "/api/v2/repositoryRoles"; +const ROLE1_URL = "http://localhost:8081/api/v2/repositoryRoles/SPECIALROLE"; + +const error = new Error("FEHLER!"); + describe("repository roles fetch()", () => { const mockStore = configureMockStore([thunk]); afterEach(() => { @@ -246,10 +262,6 @@ describe("repository roles fetch()", () => { }); }); - - - - it("should fail updating role on HTTP 500", () => { fetchMock.putOnce(ROLE1_URL, { status: 500 @@ -311,40 +323,95 @@ describe("repository roles fetch()", () => { }); }); -describe("repository roles reducer", () => { +describe("roles reducer", () => { it("should update state correctly according to FETCH_ROLES_SUCCESS action", () => { + const newState = reducer({}, fetchRolesSuccess(responseBody)); + expect(newState.list).toEqual({ + entries: ["SPECIALROLE", "WRITE"], + entry: { + roleCreatePermission: true, + page: 0, + pageTotal: 1, + _links: responseBody._links + } + }); + + expect(newState.byNames).toEqual({ + SPECIALROLE: role1, + WRITE: role2 + }); + + expect(newState.list.entry.roleCreatePermission).toBeTruthy(); }); it("should set roleCreatePermission to true if update link is present", () => { + const newState = reducer({}, fetchRolesSuccess(responseBody)); + expect(newState.list.entry.roleCreatePermission).toBeTruthy(); }); it("should not replace whole byNames map when fetching roles", () => { + const oldState = { + byNames: { + WRITE: role2 + } + }; + const newState = reducer(oldState, fetchRolesSuccess(responseBody)); + expect(newState.byNames["SPECIALROLE"]).toBeDefined(); + expect(newState.byNames["WRITE"]).toBeDefined(); }); it("should remove role from state when delete succeeds", () => { + const state = { + list: { + entries: ["WRITE", "SPECIALROLE"] + }, + byNames: { + SPECIALROLE: role1, + WRITE: role2 + } + }; + const newState = reducer(state, deleteRoleSuccess(role2)); + expect(newState.byNames["SPECIALROLE"]).toBeDefined(); + expect(newState.byNames["WRITE"]).toBeFalsy(); + expect(newState.list.entries).toEqual(["SPECIALROLE"]); }); it("should set roleCreatePermission to true if create link is present", () => { + const newState = reducer({}, fetchRolesSuccess(responseBody)); + expect(newState.list.entry.roleCreatePermission).toBeTruthy(); + expect(newState.list.entries).toEqual(["SPECIALROLE", "WRITE"]); + expect(newState.byNames["WRITE"]).toBeTruthy(); + expect(newState.byNames["SPECIALROLE"]).toBeTruthy(); }); it("should update state according to FETCH_ROLE_SUCCESS action", () => { - + const newState = reducer({}, fetchRoleSuccess(role2)); + expect(newState.byNames["WRITE"]).toBe(role2); }); it("should affect roles state nor the state of other roles", () => { - + const newState = reducer( + { + list: { + entries: ["SPECIALROLE"] + } + }, + fetchRoleSuccess(role2) + ); + expect(newState.byNames["WRITE"]).toBe(role2); + expect(newState.list.entries).toEqual(["SPECIALROLE"]); }); }); describe("selector tests", () => { it("should return an empty object", () => { expect(selectListAsCollection({})).toEqual({}); - expect(selectListAsCollection({ roles: { a: "a" } })).toEqual({}); + expect(selectListAsCollection({ repositoryRoles: { a: "a" } })).toEqual({}); }); it("should return a state slice collection", () => { @@ -354,7 +421,7 @@ describe("selector tests", () => { }; const state = { - roles: { + repositoryRoles: { list: { entry: collection } @@ -365,19 +432,19 @@ describe("selector tests", () => { it("should return false", () => { expect(isPermittedToCreateRoles({})).toBe(false); - expect(isPermittedToCreateRoles({ roles: { list: { entry: {} } } })).toBe( + expect(isPermittedToCreateRoles({ repositoryRoles: { list: { entry: {} } } })).toBe( false ); expect( isPermittedToCreateRoles({ - roles: { list: { entry: { roleCreatePermission: false } } } + repositoryRoles: { list: { entry: { roleCreatePermission: false } } } }) ).toBe(false); }); it("should return true", () => { const state = { - roles: { + repositoryRoles: { list: { entry: { roleCreatePermission: true @@ -388,9 +455,9 @@ describe("selector tests", () => { expect(isPermittedToCreateRoles(state)).toBe(true); }); - it("should get roles from state", () => { + it("should get repositoryRoles from state", () => { const state = { - roles: { + repositoryRoles: { list: { entries: ["a", "b"] }, @@ -403,7 +470,7 @@ describe("selector tests", () => { expect(getRolesFromState(state)).toEqual([{ name: "a" }, { name: "b" }]); }); - it("should return true, when fetch roles is pending", () => { + it("should return true, when fetch repositoryRoles is pending", () => { const state = { pending: { [FETCH_ROLES]: true @@ -412,11 +479,11 @@ describe("selector tests", () => { expect(isFetchRolesPending(state)).toEqual(true); }); - it("should return false, when fetch roles is not pending", () => { + it("should return false, when fetch repositoryRoles is not pending", () => { expect(isFetchRolesPending({})).toEqual(false); }); - it("should return error when fetch roles did fail", () => { + it("should return error when fetch repositoryRoles did fail", () => { const state = { failure: { [FETCH_ROLES]: error @@ -425,7 +492,7 @@ describe("selector tests", () => { expect(getFetchRolesFailure(state)).toEqual(error); }); - it("should return undefined when fetch roles did not fail", () => { + it("should return undefined when fetch repositoryRoles did not fail", () => { expect(getFetchRolesFailure({})).toBe(undefined); }); @@ -462,7 +529,7 @@ describe("selector tests", () => { it("should return role1", () => { const state = { - roles: { + repositoryRoles: { byNames: { role1: role1 } @@ -549,4 +616,3 @@ describe("selector tests", () => { expect(getDeleteRoleFailure({}, "role2")).toBe(undefined); }); }); - From 798eb5a846b60a6976b1ddb2441b00b3cb7206b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Thu, 9 May 2019 09:45:33 +0200 Subject: [PATCH 30/91] Replace wrong endpoint for repository roles with endpoint for verbs --- .../main/java/sonia/scm/web/VndMediaType.java | 2 +- .../AvailableRepositoryPermissionsDto.java | 31 ------------------ .../api/v2/resources/IndexDtoGenerator.java | 2 +- ...ource.java => RepositoryVerbResource.java} | 21 ++++++------ .../api/v2/resources/RepositoryVerbsDto.java | 19 +++++++++++ .../scm/api/v2/resources/ResourceLinks.java | 32 +++++++++---------- .../api/v2/resources/ResourceLinksMock.java | 2 +- 7 files changed, 49 insertions(+), 60 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{RepositoryPermissionResource.java => RepositoryVerbResource.java} (52%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbsDto.java diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index cad62821ed..c9696b0641 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -34,7 +34,7 @@ public class VndMediaType { public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX; public static final String CONFIG = PREFIX + "config" + SUFFIX; - public static final String REPOSITORY_PERMISSION_COLLECTION = PREFIX + "repositoryPermissionCollection" + SUFFIX; + public static final String REPOSITORY_VERB_COLLECTION = PREFIX + "repositoryVerbCollection" + SUFFIX; public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX; public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java deleted file mode 100644 index 681be90a38..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package sonia.scm.api.v2.resources; - -import de.otto.edison.hal.HalRepresentation; -import de.otto.edison.hal.Links; -import sonia.scm.repository.RepositoryRole; - -import java.util.Collection; - -public class AvailableRepositoryPermissionsDto extends HalRepresentation { - private final Collection<String> availableVerbs; - private final Collection<RepositoryRole> availableRoles; - - public AvailableRepositoryPermissionsDto(Collection<String> availableVerbs, Collection<RepositoryRole> availableRoles) { - this.availableVerbs = availableVerbs; - this.availableRoles = availableRoles; - } - - public Collection<String> getAvailableVerbs() { - return availableVerbs; - } - - public Collection<RepositoryRole> getAvailableRoles() { - return availableRoles; - } - - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 40c04f3de7..634de9381c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -59,7 +59,7 @@ public class IndexDtoGenerator extends HalAppenderMapper { if (PermissionPermissions.list().isPermitted()) { builder.single(link("permissions", resourceLinks.permissions().self())); } - builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self())); + builder.single(link("repositoryVerbs", resourceLinks.repositoryVerbs().self())); builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self())); builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self())); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbResource.java similarity index 52% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbResource.java index e5734085ca..4d4de067e5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbResource.java @@ -12,18 +12,18 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; /** - * RESTful Web Service Resource to get available repository types. + * RESTful Web Service Resource to get available repository verbs. */ -@Path(RepositoryPermissionResource.PATH) -public class RepositoryPermissionResource { +@Path(RepositoryVerbResource.PATH) +public class RepositoryVerbResource { - static final String PATH = "v2/repositoryPermissions/"; + static final String PATH = "v2/repositoryVerbs/"; private final RepositoryPermissionProvider repositoryPermissionProvider; private final ResourceLinks resourceLinks; @Inject - public RepositoryPermissionResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { + public RepositoryVerbResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { this.repositoryPermissionProvider = repositoryPermissionProvider; this.resourceLinks = resourceLinks; } @@ -34,10 +34,11 @@ public class RepositoryPermissionResource { @ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 500, condition = "internal server error") }) - @Produces(VndMediaType.REPOSITORY_PERMISSION_COLLECTION) - public AvailableRepositoryPermissionsDto get() { - AvailableRepositoryPermissionsDto dto = new AvailableRepositoryPermissionsDto(repositoryPermissionProvider.availableVerbs(), repositoryPermissionProvider.availableRoles()); - dto.add(Links.linkingTo().self(resourceLinks.availableRepositoryPermissions().self()).build()); - return dto; + @Produces(VndMediaType.REPOSITORY_VERB_COLLECTION) + public RepositoryVerbsDto getAll() { + return new RepositoryVerbsDto( + Links.linkingTo().self(resourceLinks.repositoryVerbs().self()).build(), + repositoryPermissionProvider.availableVerbs() + ); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbsDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbsDto.java new file mode 100644 index 0000000000..cbfa61c9ef --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryVerbsDto.java @@ -0,0 +1,19 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; + +import java.util.Collection; + +public class RepositoryVerbsDto extends HalRepresentation { + private final Collection<String> verbs; + + public RepositoryVerbsDto(Links links, Collection<String> verbs) { + super(links); + this.verbs = verbs; + } + + public Collection<String> getVerbs() { + return verbs; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 141645a09c..5e7e89a2e6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -523,6 +523,22 @@ class ResourceLinks { } } + RepositoryVerbLinks repositoryVerbs() { + return new RepositoryVerbLinks(scmPathInfoStore.get()); + } + + static class RepositoryVerbLinks { + private final LinkBuilder repositoryVerbLinkBuilder; + + RepositoryVerbLinks(ScmPathInfo pathInfo) { + repositoryVerbLinkBuilder = new LinkBuilder(pathInfo, RepositoryVerbResource.class); + } + + String self() { + return repositoryVerbLinkBuilder.method("getAll").parameters().href(); + } + } + RepositoryRoleLinks repositoryRole() { return new RepositoryRoleLinks(scmPathInfoStore.get()); } @@ -710,20 +726,4 @@ class ResourceLinks { return permissionsLinkBuilder.method("getAll").parameters().href(); } } - - public AvailableRepositoryPermissionLinks availableRepositoryPermissions() { - return new AvailableRepositoryPermissionLinks(scmPathInfoStore.get()); - } - - static class AvailableRepositoryPermissionLinks { - private final LinkBuilder linkBuilder; - - AvailableRepositoryPermissionLinks(ScmPathInfo scmPathInfo) { - this.linkBuilder = new LinkBuilder(scmPathInfo, RepositoryPermissionResource.class); - } - - String self() { - return linkBuilder.method("get").parameters().href(); - } - } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index b6ce087050..6950d882f4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -42,7 +42,7 @@ public class ResourceLinksMock { when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); - when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo)); + when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(uriInfo)); when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); From 872fe3a86689d137d5b21fb7c9f063e017d56dde Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 13:40:42 +0200 Subject: [PATCH 31/91] simplified module --- scm-ui/src/users/modules/users.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index 71235c689c..f515fb27e8 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -324,12 +324,10 @@ function deleteUserInEntries(users: [], userName: string) { } const reducerByName = (state: any, username: string, newUserState: any) => { - const newUsersByNames = { + return { ...state, [username]: newUserState }; - - return newUsersByNames; }; function listReducer(state: any = {}, action: any = {}) { @@ -341,7 +339,7 @@ function listReducer(state: any = {}, action: any = {}) { ...state, entries: userNames, entry: { - userCreatePermission: action.payload._links.create ? true : false, + userCreatePermission: !!action.payload._links.create, page: action.payload.page, pageTotal: action.payload.pageTotal, _links: action.payload._links @@ -379,11 +377,10 @@ function byNamesReducer(state: any = {}, action: any = {}) { return reducerByName(state, action.payload.name, action.payload); case DELETE_USER_SUCCESS: - const newUserByNames = deleteUserInUsersByNames( + return deleteUserInUsersByNames( state, action.payload.name ); - return newUserByNames; default: return state; @@ -417,11 +414,7 @@ export const selectListAsCollection = (state: Object): PagedCollection => { }; export const isPermittedToCreateUsers = (state: Object): boolean => { - const permission = selectListEntry(state).userCreatePermission; - if (permission) { - return true; - } - return false; + return !!selectListEntry(state).userCreatePermission; }; export function getUsersFromState(state: Object) { From 2ad478c0af74b5a6c4ff5574ea5c374f48cfab49 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 13:41:42 +0200 Subject: [PATCH 32/91] changed fixed getRolesLink to dynamic one --- scm-ui/src/modules/indexResource.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index 909d356752..0facb51faa 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -152,8 +152,7 @@ export function getSvnConfigLink(state: Object) { } export function getRolesLink(state: Object) { - //return getLink(state, "availableRepositoryPermissions"); - return "http://localhost:8081/scm/api/v2/repository-roles"; // TODO + return getLink(state, "repositoryRoles"); } export function getUserAutoCompleteLink(state: Object): string { From 1a7b199299d4b65861976053c060356ee540700c Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 13:43:04 +0200 Subject: [PATCH 33/91] renamed role1 and removed verbs --- scm-ui/src/config/modules/roles.test.js | 106 ++++++++++++++++-------- 1 file changed, 70 insertions(+), 36 deletions(-) diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/modules/roles.test.js index 2d2e30012c..f7654ef38b 100644 --- a/scm-ui/src/config/modules/roles.test.js +++ b/scm-ui/src/config/modules/roles.test.js @@ -49,37 +49,19 @@ import reducer, { isPermittedToCreateRoles } from "./roles"; -const verbs = [ - "createPullRequest", - "readPullRequest", - "commentPullRequest", - "modifyPullRequest", - "mergePullRequest", - "git", - "hg", - "read", - "modify", - "delete", - "pull", - "push", - "permissionRead", - "permissionWrite", - "*" -]; - const role1 = { - name: "SPECIALROLE", + name: "specialrole", verbs: ["read", "pull", "push", "readPullRequest"], system: false, _links: { self: { - href: "http://localhost:8081/scm/api/v2/repositoryRoles/SPECIALROLE" + href: "http://localhost:8081/scm/api/v2/repositoryRoles/specialrole" }, delete: { - href: "http://localhost:8081/scm/api/v2/repositoryRoles/SPECIALROLE" + href: "http://localhost:8081/scm/api/v2/repositoryRoles/specialrole" }, update: { - href: "http://localhost:8081/scm/api/v2/repositoryRoles/SPECIALROLE" + href: "http://localhost:8081/scm/api/v2/repositoryRoles/specialrole" } } }; @@ -134,7 +116,7 @@ const response = { const URL = "repositoryRoles"; const ROLES_URL = "/api/v2/repositoryRoles"; -const ROLE1_URL = "http://localhost:8081/api/v2/repositoryRoles/SPECIALROLE"; +const ROLE1_URL = "http://localhost:8081/api/v2/repositoryRoles/specialrole"; const error = new Error("FEHLER!"); @@ -178,6 +160,58 @@ describe("repository roles fetch()", () => { }); }); + it("should sucessfully fetch single role by name", () => { + fetchMock.getOnce(ROLES_URL + "/specialrole", role1); + + const store = mockStore({}); + return store.dispatch(fetchRoleByName(URL, "specialrole")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_ROLE_PENDING); + expect(actions[1].type).toEqual(FETCH_ROLE_SUCCESS); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should fail fetching single role by name on HTTP 500", () => { + fetchMock.getOnce(ROLES_URL + "/specialrole", { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchRoleByName(URL, "specialrole")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_ROLE_PENDING); + expect(actions[1].type).toEqual(FETCH_ROLE_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should sucessfully fetch single role", () => { + fetchMock.getOnce(ROLE1_URL, role1); + + const store = mockStore({}); + return store.dispatch(fetchRoleByLink(role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_ROLE_PENDING); + expect(actions[1].type).toEqual(FETCH_ROLE_SUCCESS); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should fail fetching single role on HTTP 500", () => { + fetchMock.getOnce(ROLE1_URL, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchRoleByLink(role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_ROLE_PENDING); + expect(actions[1].type).toEqual(FETCH_ROLE_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + it("should add a role successfully", () => { // unmatched fetchMock.postOnce(ROLES_URL, { @@ -291,7 +325,7 @@ describe("repository roles fetch()", () => { }); }); - it("should call the callback, after successful delete", () => { + it("should call the callback after successful delete", () => { fetchMock.deleteOnce(ROLE1_URL, { status: 204 }); @@ -359,34 +393,34 @@ describe("roles reducer", () => { }; const newState = reducer(oldState, fetchRolesSuccess(responseBody)); - expect(newState.byNames["SPECIALROLE"]).toBeDefined(); + expect(newState.byNames["specialrole"]).toBeDefined(); expect(newState.byNames["WRITE"]).toBeDefined(); }); it("should remove role from state when delete succeeds", () => { const state = { list: { - entries: ["WRITE", "SPECIALROLE"] + entries: ["WRITE", "specialrole"] }, byNames: { - SPECIALROLE: role1, + specialrole: role1, WRITE: role2 } }; const newState = reducer(state, deleteRoleSuccess(role2)); - expect(newState.byNames["SPECIALROLE"]).toBeDefined(); + expect(newState.byNames["specialrole"]).toBeDefined(); expect(newState.byNames["WRITE"]).toBeFalsy(); - expect(newState.list.entries).toEqual(["SPECIALROLE"]); + expect(newState.list.entries).toEqual(["specialrole"]); }); it("should set roleCreatePermission to true if create link is present", () => { const newState = reducer({}, fetchRolesSuccess(responseBody)); expect(newState.list.entry.roleCreatePermission).toBeTruthy(); - expect(newState.list.entries).toEqual(["SPECIALROLE", "WRITE"]); + expect(newState.list.entries).toEqual(["specialrole", "WRITE"]); expect(newState.byNames["WRITE"]).toBeTruthy(); - expect(newState.byNames["SPECIALROLE"]).toBeTruthy(); + expect(newState.byNames["specialrole"]).toBeTruthy(); }); it("should update state according to FETCH_ROLE_SUCCESS action", () => { @@ -398,13 +432,13 @@ describe("roles reducer", () => { const newState = reducer( { list: { - entries: ["SPECIALROLE"] + entries: ["specialrole"] } }, fetchRoleSuccess(role2) ); expect(newState.byNames["WRITE"]).toBe(role2); - expect(newState.list.entries).toEqual(["SPECIALROLE"]); + expect(newState.list.entries).toEqual(["specialrole"]); }); }); @@ -432,9 +466,9 @@ describe("selector tests", () => { it("should return false", () => { expect(isPermittedToCreateRoles({})).toBe(false); - expect(isPermittedToCreateRoles({ repositoryRoles: { list: { entry: {} } } })).toBe( - false - ); + expect( + isPermittedToCreateRoles({ repositoryRoles: { list: { entry: {} } } }) + ).toBe(false); expect( isPermittedToCreateRoles({ repositoryRoles: { list: { entry: { roleCreatePermission: false } } } From 96fcb55c65766c304599dd015847f5836216eff7 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 13:46:02 +0200 Subject: [PATCH 34/91] added PermissionRoleRow --- .../components/table/PermissionRoleRow.js | 26 +++++++++++++++++++ .../components/table/PermissionRoleTable.js | 9 ++++--- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 scm-ui/src/config/components/table/PermissionRoleRow.js diff --git a/scm-ui/src/config/components/table/PermissionRoleRow.js b/scm-ui/src/config/components/table/PermissionRoleRow.js new file mode 100644 index 0000000000..7e3944fc4b --- /dev/null +++ b/scm-ui/src/config/components/table/PermissionRoleRow.js @@ -0,0 +1,26 @@ +// @flow +import React from "react"; +import { Link } from "react-router-dom"; +import type { Role } from "@scm-manager/ui-types"; + +type Props = { + role: Role +}; + +class PermissionRoleRow extends React.Component<Props> { + renderLink(to: string, label: string) { + return <Link to={to}>{label}</Link>; + } + + render() { + const { role } = this.props; + const to = `./${encodeURIComponent(role.name)}/info`; + return ( + <tr> + <td>{this.renderLink(to, role.name)}</td> + </tr> + ); + } +} + +export default PermissionRoleRow; diff --git a/scm-ui/src/config/components/table/PermissionRoleTable.js b/scm-ui/src/config/components/table/PermissionRoleTable.js index aad77a5654..436fa2e1d5 100644 --- a/scm-ui/src/config/components/table/PermissionRoleTable.js +++ b/scm-ui/src/config/components/table/PermissionRoleTable.js @@ -2,10 +2,12 @@ import React from "react"; import { translate } from "react-i18next"; import type { Role } from "@scm-manager/ui-types"; +import PermissionRoleRow from "./PermissionRoleRow"; type Props = { - t: string => string, - roles: Role[] + roles: Role[], + + t: string => string }; class PermissionRoleTable extends React.Component<Props> { @@ -16,12 +18,11 @@ class PermissionRoleTable extends React.Component<Props> { <thead> <tr> <th>{t("role.form.name")}</th> - <th>{t("role.form.permissions")}</th> </tr> </thead> <tbody> {roles.map((role, index) => { - return <p key={index}>role</p>; + return <PermissionRoleRow key={index} role={role} />; })} </tbody> </table> From 7db1e4aad0a6c6b6fc4a5c17ea85845653bee4c7 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 14:55:28 +0200 Subject: [PATCH 35/91] corrected content type + removed unsupported filter --- .../containers/GlobalPermissionRoles.js | 33 +++++++------------ scm-ui/src/config/modules/roles.js | 11 ++++--- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalPermissionRoles.js b/scm-ui/src/config/containers/GlobalPermissionRoles.js index 63c8385daa..e9fb0bb47f 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoles.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoles.js @@ -3,7 +3,7 @@ import React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; import type { History } from "history"; -import type {Role, PagedCollection} from "@scm-manager/ui-types"; +import type { Role, PagedCollection } from "@scm-manager/ui-types"; import { fetchRolesByPage, getRolesFromState, @@ -35,20 +35,15 @@ type Props = { // context objects t: string => string, history: History, - location: any, // dispatch functions - fetchRolesByPage: (link: string, page: number, filter?: string) => void + fetchRolesByPage: (link: string, page: number) => void }; class GlobalPermissionRoles extends React.Component<Props> { componentDidMount() { - const { fetchRolesByPage, rolesLink, page, location } = this.props; - fetchRolesByPage( - rolesLink, - page, - urls.getQueryStringFromLocation(location) - ); + const { fetchRolesByPage, rolesLink, page } = this.props; + fetchRolesByPage(rolesLink, page); } render() { @@ -68,28 +63,24 @@ class GlobalPermissionRoles extends React.Component<Props> { } renderPermissionsTable() { - const { roles, list, page, location, t } = this.props; + const { roles, list, page, t } = this.props; if (roles && roles.length > 0) { return ( <> <PermissionRoleTable roles={roles} /> - <LinkPaginator - collection={list} - page={page} - filter={urls.getQueryStringFromLocation(location)} - /> + <LinkPaginator collection={list} page={page} /> </> ); } - return <Notification type="info">{t("roles.noPermissionRoles")}</Notification>; + return ( + <Notification type="info">{t("roles.noPermissionRoles")}</Notification> + ); } renderCreateButton() { const { canAddRoles, t } = this.props; if (canAddRoles) { - return ( - <CreateButton label={t("roles.createButton")} link="/create" /> - ); + return <CreateButton label={t("roles.createButton")} link="/create" />; } return null; } @@ -118,8 +109,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchRolesByPage: (link: string, page: number, filter?: string) => { - dispatch(fetchRolesByPage(link, page, filter)); + fetchRolesByPage: (link: string, page: number) => { + dispatch(fetchRolesByPage(link, page)); } }; }; diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index af81190bb4..7ea871ff09 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -33,7 +33,7 @@ export const DELETE_ROLE_PENDING = `${DELETE_ROLE}_${types.PENDING_SUFFIX}`; export const DELETE_ROLE_SUCCESS = `${DELETE_ROLE}_${types.SUCCESS_SUFFIX}`; export const DELETE_ROLE_FAILURE = `${DELETE_ROLE}_${types.FAILURE_SUFFIX}`; -const CONTENT_TYPE_ROLE = "application/vnd.scmm-role+json;v=2"; +const CONTENT_TYPE_ROLE = "application/vnd.scmm-repositoryRole+json;v=2"; // fetch roles export function fetchRolesPending(): Action { @@ -184,7 +184,7 @@ export function createRole(link: string, role: Role, callback?: () => void) { }; } -// modify group +// modify role export function modifyRolePending(role: Role): Action { return { type: MODIFY_ROLE_PENDING, @@ -371,7 +371,10 @@ function byNamesReducer(state: any = {}, action: any = {}) { return reducerByName(state, action.payload.name, action.payload); case DELETE_ROLE_SUCCESS: - return deleteRoleInRolesByNames(state, action.payload.name); + return deleteRoleInRolesByNames( + state, + action.payload.name + ); default: return state; @@ -404,7 +407,7 @@ export const selectListAsCollection = (state: Object): PagedCollection => { }; export const isPermittedToCreateRoles = (state: Object): boolean => { - return selectListEntry(state).roleCreatePermission; + return !!selectListEntry(state).roleCreatePermission; }; export function getRolesFromState(state: Object) { From b8d73b91eaf48cd3ade162fe67da89bfc9853dcc Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 15:11:55 +0200 Subject: [PATCH 36/91] added baseUrl for info link --- .../components/table/PermissionRoleRow.js | 5 +++-- .../components/table/PermissionRoleTable.js | 17 ++++++++++------- scm-ui/src/config/containers/Config.js | 6 +++++- .../config/containers/GlobalPermissionRoles.js | 11 ++++++----- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/scm-ui/src/config/components/table/PermissionRoleRow.js b/scm-ui/src/config/components/table/PermissionRoleRow.js index 7e3944fc4b..a47d7e7e27 100644 --- a/scm-ui/src/config/components/table/PermissionRoleRow.js +++ b/scm-ui/src/config/components/table/PermissionRoleRow.js @@ -4,6 +4,7 @@ import { Link } from "react-router-dom"; import type { Role } from "@scm-manager/ui-types"; type Props = { + baseUrl: string, role: Role }; @@ -13,8 +14,8 @@ class PermissionRoleRow extends React.Component<Props> { } render() { - const { role } = this.props; - const to = `./${encodeURIComponent(role.name)}/info`; + const { baseUrl, role } = this.props; + const to = `${baseUrl}/${encodeURIComponent(role.name)}/info`; return ( <tr> <td>{this.renderLink(to, role.name)}</td> diff --git a/scm-ui/src/config/components/table/PermissionRoleTable.js b/scm-ui/src/config/components/table/PermissionRoleTable.js index 436fa2e1d5..3c9dde1cc5 100644 --- a/scm-ui/src/config/components/table/PermissionRoleTable.js +++ b/scm-ui/src/config/components/table/PermissionRoleTable.js @@ -5,6 +5,7 @@ import type { Role } from "@scm-manager/ui-types"; import PermissionRoleRow from "./PermissionRoleRow"; type Props = { + baseUrl: string, roles: Role[], t: string => string @@ -12,18 +13,20 @@ type Props = { class PermissionRoleTable extends React.Component<Props> { render() { - const { roles, t } = this.props; + const { baseUrl, roles, t } = this.props; return ( <table className="card-table table is-hoverable is-fullwidth"> <thead> - <tr> - <th>{t("role.form.name")}</th> - </tr> + <tr> + <th>{t("role.form.name")}</th> + </tr> </thead> <tbody> - {roles.map((role, index) => { - return <PermissionRoleRow key={index} role={role} />; - })} + {roles.map((role, index) => { + return ( + <PermissionRoleRow key={index} baseUrl={baseUrl} role={role} /> + ); + })} </tbody> </table> ); diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 388b9650d7..53dc62f144 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -51,7 +51,11 @@ class Config extends React.Component<Props> { <Route path={`${url}/roles`} exact - component={GlobalPermissionRoles} + render={() => ( + <GlobalPermissionRoles + baseUrl={`${url}/roles`} + /> + )} /> <ExtensionPoint name="config.route" diff --git a/scm-ui/src/config/containers/GlobalPermissionRoles.js b/scm-ui/src/config/containers/GlobalPermissionRoles.js index e9fb0bb47f..9fe513e797 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoles.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoles.js @@ -1,6 +1,7 @@ // @flow import React from "react"; import { connect } from "react-redux"; +import {withRouter} from "react-router-dom"; import { translate } from "react-i18next"; import type { History } from "history"; import type { Role, PagedCollection } from "@scm-manager/ui-types"; @@ -22,8 +23,8 @@ import { } from "@scm-manager/ui-components"; import PermissionRoleTable from "../components/table/PermissionRoleTable"; import { getRolesLink } from "../../modules/indexResource"; - type Props = { + baseUrl: string, roles: Role[], loading: boolean, error: Error, @@ -63,11 +64,11 @@ class GlobalPermissionRoles extends React.Component<Props> { } renderPermissionsTable() { - const { roles, list, page, t } = this.props; + const { baseUrl, roles, list, page, t } = this.props; if (roles && roles.length > 0) { return ( <> - <PermissionRoleTable roles={roles} /> + <PermissionRoleTable baseUrl={baseUrl} roles={roles} /> <LinkPaginator collection={list} page={page} /> </> ); @@ -115,7 +116,7 @@ const mapDispatchToProps = dispatch => { }; }; -export default connect( +export default withRouter(connect( mapStateToProps, mapDispatchToProps -)(translate("config")(GlobalPermissionRoles)); +)(translate("config")(GlobalPermissionRoles))); From 520b5a4f9067eebb6979465ecb4b009a8cd296b4 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 15:25:47 +0200 Subject: [PATCH 37/91] removed unnecessary confusing modify and delete actions --- scm-ui/src/config/modules/roles.js | 196 ++---------------------- scm-ui/src/config/modules/roles.test.js | 187 +--------------------- 2 files changed, 22 insertions(+), 361 deletions(-) diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index 7ea871ff09..5fd96f2105 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -22,17 +22,6 @@ export const CREATE_ROLE_SUCCESS = `${CREATE_ROLE}_${types.SUCCESS_SUFFIX}`; export const CREATE_ROLE_FAILURE = `${CREATE_ROLE}_${types.FAILURE_SUFFIX}`; export const CREATE_ROLE_RESET = `${CREATE_ROLE}_${types.RESET_SUFFIX}`; -export const MODIFY_ROLE = "scm/roles/MODIFY_ROLE"; -export const MODIFY_ROLE_PENDING = `${MODIFY_ROLE}_${types.PENDING_SUFFIX}`; -export const MODIFY_ROLE_SUCCESS = `${MODIFY_ROLE}_${types.SUCCESS_SUFFIX}`; -export const MODIFY_ROLE_FAILURE = `${MODIFY_ROLE}_${types.FAILURE_SUFFIX}`; -export const MODIFY_ROLE_RESET = `${MODIFY_ROLE}_${types.RESET_SUFFIX}`; - -export const DELETE_ROLE = "scm/roles/DELETE_ROLE"; -export const DELETE_ROLE_PENDING = `${DELETE_ROLE}_${types.PENDING_SUFFIX}`; -export const DELETE_ROLE_SUCCESS = `${DELETE_ROLE}_${types.SUCCESS_SUFFIX}`; -export const DELETE_ROLE_FAILURE = `${DELETE_ROLE}_${types.FAILURE_SUFFIX}`; - const CONTENT_TYPE_ROLE = "application/vnd.scmm-repositoryRole+json;v=2"; // fetch roles @@ -184,104 +173,24 @@ export function createRole(link: string, role: Role, callback?: () => void) { }; } -// modify role -export function modifyRolePending(role: Role): Action { - return { - type: MODIFY_ROLE_PENDING, - payload: role, - itemId: role.name - }; -} - -export function modifyRoleSuccess(role: Role): Action { - return { - type: MODIFY_ROLE_SUCCESS, - payload: role, - itemId: role.name - }; -} - -export function modifyRoleFailure(role: Role, error: Error): Action { - return { - type: MODIFY_ROLE_FAILURE, - payload: { - error, - role - }, - itemId: role.name - }; -} - -export function modifyRoleReset(role: Role): Action { - return { - type: MODIFY_ROLE_RESET, - itemId: role.name - }; -} - -export function modifyRole(role: Role, callback?: () => void) { - return function(dispatch: Dispatch) { - dispatch(modifyRolePending(role)); - return apiClient - .put(role._links.update.href, role, CONTENT_TYPE_ROLE) - .then(() => { - dispatch(modifyRoleSuccess(role)); - if (callback) { - callback(); +function listReducer(state: any = {}, action: any = {}) { + switch (action.type) { + case FETCH_ROLES_SUCCESS: + const roles = action.payload._embedded.repositoryRoles; + const roleNames = roles.map(role => role.name); + return { + ...state, + entries: roleNames, + entry: { + roleCreatePermission: !!action.payload._links.create, + page: action.payload.page, + pageTotal: action.payload.pageTotal, + _links: action.payload._links } - }) - .then(() => { - dispatch(fetchRoleByLink(role)); - }) - .catch(err => { - dispatch(modifyRoleFailure(role, err)); - }); - }; -} - -// delete role -export function deleteRolePending(role: Role): Action { - return { - type: DELETE_ROLE_PENDING, - payload: role, - itemId: role.name - }; -} - -export function deleteRoleSuccess(role: Role): Action { - return { - type: DELETE_ROLE_SUCCESS, - payload: role, - itemId: role.name - }; -} - -export function deleteRoleFailure(role: Role, error: Error): Action { - return { - type: DELETE_ROLE_FAILURE, - payload: { - error, - role - }, - itemId: role.name - }; -} - -export function deleteRole(role: Role, callback?: () => void) { - return function(dispatch: any) { - dispatch(deleteRolePending(role)); - return apiClient - .delete(role._links.delete.href) - .then(() => { - dispatch(deleteRoleSuccess(role)); - if (callback) { - callback(); - } - }) - .catch(error => { - dispatch(deleteRoleFailure(role, error)); - }); - }; + }; + default: + return state; + } } function extractRolesByNames( @@ -301,22 +210,6 @@ function extractRolesByNames( return rolesByNames; } -function deleteRoleInRolesByNames(roles: {}, roleName: string) { - let newRoles = {}; - for (let rolename in roles) { - if (rolename !== roleName) newRoles[rolename] = roles[rolename]; - } - return newRoles; -} - -function deleteRoleInEntries(roles: [], roleName: string) { - let newRoles = []; - for (let role of roles) { - if (role !== roleName) newRoles.push(role); - } - return newRoles; -} - const reducerByName = (state: any, rolename: string, newRoleState: any) => { return { ...state, @@ -324,37 +217,6 @@ const reducerByName = (state: any, rolename: string, newRoleState: any) => { }; }; -function listReducer(state: any = {}, action: any = {}) { - switch (action.type) { - case FETCH_ROLES_SUCCESS: - const roles = action.payload._embedded.repositoryRoles; - const roleNames = roles.map(role => role.name); - return { - ...state, - entries: roleNames, - entry: { - roleCreatePermission: !!action.payload._links.create, - page: action.payload.page, - pageTotal: action.payload.pageTotal, - _links: action.payload._links - } - }; - - // Delete single role actions - case DELETE_ROLE_SUCCESS: - const newRoleEntries = deleteRoleInEntries( - state.entries, - action.payload.name - ); - return { - ...state, - entries: newRoleEntries - }; - default: - return state; - } -} - function byNamesReducer(state: any = {}, action: any = {}) { switch (action.type) { // Fetch all roles actions @@ -365,17 +227,9 @@ function byNamesReducer(state: any = {}, action: any = {}) { return { ...byNames }; - // Fetch single role actions case FETCH_ROLE_SUCCESS: return reducerByName(state, action.payload.name, action.payload); - - case DELETE_ROLE_SUCCESS: - return deleteRoleInRolesByNames( - state, - action.payload.name - ); - default: return state; } @@ -453,19 +307,3 @@ export function isFetchRolePending(state: Object, name: string) { export function getFetchRoleFailure(state: Object, name: string) { return getFailure(state, FETCH_ROLE, name); } - -export function isModifyRolePending(state: Object, name: string) { - return isPending(state, MODIFY_ROLE, name); -} - -export function getModifyRoleFailure(state: Object, name: string) { - return getFailure(state, MODIFY_ROLE, name); -} - -export function isDeleteRolePending(state: Object, name: string) { - return isPending(state, DELETE_ROLE, name); -} - -export function getDeleteRoleFailure(state: Object, name: string) { - return getFailure(state, DELETE_ROLE, name); -} diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/modules/roles.test.js index f7654ef38b..38da47c9ec 100644 --- a/scm-ui/src/config/modules/roles.test.js +++ b/scm-ui/src/config/modules/roles.test.js @@ -16,14 +16,6 @@ import reducer, { CREATE_ROLE_PENDING, CREATE_ROLE_SUCCESS, CREATE_ROLE_FAILURE, - MODIFY_ROLE, - MODIFY_ROLE_PENDING, - MODIFY_ROLE_SUCCESS, - MODIFY_ROLE_FAILURE, - DELETE_ROLE, - DELETE_ROLE_PENDING, - DELETE_ROLE_SUCCESS, - DELETE_ROLE_FAILURE, fetchRoles, getFetchRolesFailure, getRolesFromState, @@ -38,13 +30,6 @@ import reducer, { isCreateRolePending, getCreateRoleFailure, getRoleByName, - modifyRole, - isModifyRolePending, - getModifyRoleFailure, - deleteRole, - isDeleteRolePending, - deleteRoleSuccess, - getDeleteRoleFailure, selectListAsCollection, isPermittedToCreateRoles } from "./roles"; @@ -120,7 +105,7 @@ const ROLE1_URL = "http://localhost:8081/api/v2/repositoryRoles/specialrole"; const error = new Error("FEHLER!"); -describe("repository roles fetch()", () => { +describe("repository roles fetch", () => { const mockStore = configureMockStore([thunk]); afterEach(() => { fetchMock.reset(); @@ -262,107 +247,14 @@ describe("repository roles fetch()", () => { expect(callMe).toBe("yeah"); }); }); - - it("successfully update role", () => { - fetchMock.putOnce(ROLE1_URL, { - status: 204 - }); - fetchMock.getOnce(ROLE1_URL, role1); - - const store = mockStore({}); - return store.dispatch(modifyRole(role1)).then(() => { - const actions = store.getActions(); - expect(actions.length).toBe(3); - expect(actions[0].type).toEqual(MODIFY_ROLE_PENDING); - expect(actions[1].type).toEqual(MODIFY_ROLE_SUCCESS); - expect(actions[2].type).toEqual(FETCH_ROLE_PENDING); - }); - }); - - it("should call callback, after successful modified role", () => { - fetchMock.putOnce(ROLE1_URL, { - status: 204 - }); - fetchMock.getOnce(ROLE1_URL, role1); - - let called = false; - const callMe = () => { - called = true; - }; - - const store = mockStore({}); - return store.dispatch(modifyRole(role1, callMe)).then(() => { - expect(called).toBeTruthy(); - }); - }); - - it("should fail updating role on HTTP 500", () => { - fetchMock.putOnce(ROLE1_URL, { - status: 500 - }); - - const store = mockStore({}); - return store.dispatch(modifyRole(role1)).then(() => { - const actions = store.getActions(); - expect(actions[0].type).toEqual(MODIFY_ROLE_PENDING); - expect(actions[1].type).toEqual(MODIFY_ROLE_FAILURE); - expect(actions[1].payload).toBeDefined(); - }); - }); - - it("should delete successfully role1", () => { - fetchMock.deleteOnce(ROLE1_URL, { - status: 204 - }); - - const store = mockStore({}); - return store.dispatch(deleteRole(role1)).then(() => { - const actions = store.getActions(); - expect(actions.length).toBe(2); - expect(actions[0].type).toEqual(DELETE_ROLE_PENDING); - expect(actions[0].payload).toBe(role1); - expect(actions[1].type).toEqual(DELETE_ROLE_SUCCESS); - }); - }); - - it("should call the callback after successful delete", () => { - fetchMock.deleteOnce(ROLE1_URL, { - status: 204 - }); - - let called = false; - const callMe = () => { - called = true; - }; - - const store = mockStore({}); - return store.dispatch(deleteRole(role1, callMe)).then(() => { - expect(called).toBeTruthy(); - }); - }); - - it("should fail to delete role1", () => { - fetchMock.deleteOnce(ROLE1_URL, { - status: 500 - }); - - const store = mockStore({}); - return store.dispatch(deleteRole(role1)).then(() => { - const actions = store.getActions(); - expect(actions[0].type).toEqual(DELETE_ROLE_PENDING); - expect(actions[0].payload).toBe(role1); - expect(actions[1].type).toEqual(DELETE_ROLE_FAILURE); - expect(actions[1].payload).toBeDefined(); - }); - }); }); -describe("roles reducer", () => { +describe("repository roles reducer", () => { it("should update state correctly according to FETCH_ROLES_SUCCESS action", () => { const newState = reducer({}, fetchRolesSuccess(responseBody)); expect(newState.list).toEqual({ - entries: ["SPECIALROLE", "WRITE"], + entries: ["specialrole", "WRITE"], entry: { roleCreatePermission: true, page: 0, @@ -372,7 +264,7 @@ describe("roles reducer", () => { }); expect(newState.byNames).toEqual({ - SPECIALROLE: role1, + specialrole: role1, WRITE: role2 }); @@ -397,23 +289,6 @@ describe("roles reducer", () => { expect(newState.byNames["WRITE"]).toBeDefined(); }); - it("should remove role from state when delete succeeds", () => { - const state = { - list: { - entries: ["WRITE", "specialrole"] - }, - byNames: { - specialrole: role1, - WRITE: role2 - } - }; - - const newState = reducer(state, deleteRoleSuccess(role2)); - expect(newState.byNames["specialrole"]).toBeDefined(); - expect(newState.byNames["WRITE"]).toBeFalsy(); - expect(newState.list.entries).toEqual(["specialrole"]); - }); - it("should set roleCreatePermission to true if create link is present", () => { const newState = reducer({}, fetchRolesSuccess(responseBody)); @@ -442,7 +317,7 @@ describe("roles reducer", () => { }); }); -describe("selector tests", () => { +describe("repository roles selector", () => { it("should return an empty object", () => { expect(selectListAsCollection({})).toEqual({}); expect(selectListAsCollection({ repositoryRoles: { a: "a" } })).toEqual({}); @@ -597,56 +472,4 @@ describe("selector tests", () => { it("should return undefined when fetch role2 did not fail", () => { expect(getFetchRoleFailure({}, "role2")).toBe(undefined); }); - - it("should return true, when modify role1 is pending", () => { - const state = { - pending: { - [MODIFY_ROLE + "/role1"]: true - } - }; - expect(isModifyRolePending(state, "role1")).toEqual(true); - }); - - it("should return false, when modify role1 is not pending", () => { - expect(isModifyRolePending({}, "role1")).toEqual(false); - }); - - it("should return error when modify role1 did fail", () => { - const state = { - failure: { - [MODIFY_ROLE + "/role1"]: error - } - }; - expect(getModifyRoleFailure(state, "role1")).toEqual(error); - }); - - it("should return undefined when modify role1 did not fail", () => { - expect(getModifyRoleFailure({}, "role1")).toBe(undefined); - }); - - it("should return true, when delete role2 is pending", () => { - const state = { - pending: { - [DELETE_ROLE + "/role2"]: true - } - }; - expect(isDeleteRolePending(state, "role2")).toEqual(true); - }); - - it("should return false, when delete role2 is not pending", () => { - expect(isDeleteRolePending({}, "role2")).toEqual(false); - }); - - it("should return error when delete role2 did fail", () => { - const state = { - failure: { - [DELETE_ROLE + "/role2"]: error - } - }; - expect(getDeleteRoleFailure(state, "role2")).toEqual(error); - }); - - it("should return undefined when delete role2 did not fail", () => { - expect(getDeleteRoleFailure({}, "role2")).toBe(undefined); - }); }); From 04bc1f6fe8d483863848c57d7813aad0fb276d13 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 15:28:38 +0200 Subject: [PATCH 38/91] fixed tests --- scm-ui/src/config/modules/roles.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/modules/roles.test.js index 38da47c9ec..e9a12bc30a 100644 --- a/scm-ui/src/config/modules/roles.test.js +++ b/scm-ui/src/config/modules/roles.test.js @@ -101,7 +101,7 @@ const response = { const URL = "repositoryRoles"; const ROLES_URL = "/api/v2/repositoryRoles"; -const ROLE1_URL = "http://localhost:8081/api/v2/repositoryRoles/specialrole"; +const ROLE1_URL = "http://localhost:8081/scm/api/v2/repositoryRoles/specialrole"; const error = new Error("FEHLER!"); From ae06509328758471d1a213223cdbd0c4663d3b04 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:02:02 +0200 Subject: [PATCH 39/91] added new roles reducer --- scm-ui/src/createReduxStore.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index eb0586d657..70ad9abd5b 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -15,6 +15,7 @@ import pending from "./modules/pending"; import failure from "./modules/failure"; import permissions from "./repos/permissions/modules/permissions"; import config from "./config/modules/config"; +import roles from "./config/modules/roles"; import namespaceStrategies from "./config/modules/namespaceStrategies"; import indexResources from "./modules/indexResource"; @@ -39,6 +40,7 @@ function createReduxStore(history: BrowserHistory) { groups, auth, config, + roles, sources, namespaceStrategies }); From 49a8a167c71c235b3879314209ccf23aef4d2482 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:02:14 +0200 Subject: [PATCH 40/91] fixed tests --- .../RepositoryRoleRootResourceTest.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java index 0489717926..af968efd1f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java @@ -117,9 +117,9 @@ public class RepositoryRoleRootResourceTest { .contains( "\"name\":\"" + CUSTOM_ROLE + "\"", "\"verbs\":[\"verb\"]", - "\"self\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" + "\"self\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}" ); } @@ -135,11 +135,11 @@ public class RepositoryRoleRootResourceTest { .contains( "\"name\":\"" + SYSTEM_ROLE + "\"", "\"verbs\":[\"admin\"]", - "\"self\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + "\"self\":{\"href\":\"/v2/repositoryRoles/" + SYSTEM_ROLE + "\"}" ) .doesNotContain( - "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}" ); } @@ -215,7 +215,7 @@ public class RepositoryRoleRootResourceTest { assertThat(createCaptor.getValue().getName()).isEqualTo("newRole"); assertThat(createCaptor.getValue().getVerbs()).containsExactly("write", "push"); Object location = response.getOutputHeaders().getFirst("Location"); - assertThat(location).isEqualTo(create("/v2/repository-roles/newRole")); + assertThat(location).isEqualTo(create("/v2/repositoryRoles/newRole")); } @Test @@ -245,12 +245,12 @@ public class RepositoryRoleRootResourceTest { "\"name\":\"" + SYSTEM_ROLE + "\"", "\"verbs\":[\"verb\"]", "\"verbs\":[\"admin\"]", - "\"self\":{\"href\":\"/v2/repository-roles", - "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"create\":{\"href\":\"/v2/repository-roles/\"}" + "\"self\":{\"href\":\"/v2/repositoryRoles", + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"create\":{\"href\":\"/v2/repositoryRoles/\"}" ) .doesNotContain( - "\"delete\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + SYSTEM_ROLE + "\"}" ); } From cd18dfe824031d3ccdf967b9322185e8e41c80dd Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:54:49 +0200 Subject: [PATCH 41/91] fixed state calls --- scm-ui/src/config/modules/roles.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index 5fd96f2105..c241ca15e9 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -242,8 +242,8 @@ export default combineReducers({ // selectors const selectList = (state: Object) => { - if (state.repositoryRoles && state.repositoryRoles.list) { - return state.repositoryRoles.list; + if (state.roles && state.roles.list) { + return state.roles.list; } return {}; }; @@ -272,7 +272,7 @@ export function getRolesFromState(state: Object) { const roleEntries: Role[] = []; for (let roleName of roleNames) { - roleEntries.push(state.repositoryRoles.byNames[roleName]); + roleEntries.push(state.roles.byNames[roleName]); } return roleEntries; @@ -295,8 +295,8 @@ export function getCreateRoleFailure(state: Object) { } export function getRoleByName(state: Object, name: string) { - if (state.repositoryRoles && state.repositoryRoles.byNames) { - return state.repositoryRoles.byNames[name]; + if (state.roles && state.roles.byNames) { + return state.roles.byNames[name]; } } From b2a6d6fad13821b63136cc2e0b0a4aba010d841d Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:55:21 +0200 Subject: [PATCH 42/91] added baseUrl to create button --- scm-ui/src/config/containers/GlobalPermissionRoles.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalPermissionRoles.js b/scm-ui/src/config/containers/GlobalPermissionRoles.js index 9fe513e797..eb579f6e69 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoles.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoles.js @@ -79,9 +79,9 @@ class GlobalPermissionRoles extends React.Component<Props> { } renderCreateButton() { - const { canAddRoles, t } = this.props; + const { canAddRoles, baseUrl, t } = this.props; if (canAddRoles) { - return <CreateButton label={t("roles.createButton")} link="/create" />; + return <CreateButton label={t("roles.createButton")} link={`${baseUrl}/create`} />; } return null; } From e7b7690996572b1be8fb5138adf46adf3ef41a82 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:56:01 +0200 Subject: [PATCH 43/91] added placeholder form for create and edit roles --- .../config/containers/GlobalPermissionRoleForm.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 scm-ui/src/config/containers/GlobalPermissionRoleForm.js diff --git a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js new file mode 100644 index 0000000000..ba6cd293db --- /dev/null +++ b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js @@ -0,0 +1,14 @@ +// @flow +import React from "react"; + +type Props = { + thing?: any, +}; + +class GlobalPermissionRoleForm extends React.Component<Props> { + render() { + return <p>Placeholder</p>; + } +} + +export default GlobalPermissionRoleForm; From b81726cea88cc27a1d53932ed32584fe3bd891fa Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:57:13 +0200 Subject: [PATCH 44/91] added form-route and matching path --- scm-ui/src/config/containers/Config.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 53dc62f144..ee973cf64b 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -3,15 +3,15 @@ import React from "react"; import { translate } from "react-i18next"; import { Route } from "react-router"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; - -import type { Links } from "@scm-manager/ui-types"; -import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; -import GlobalConfig from "./GlobalConfig"; -import GlobalPermissionRoles from "./GlobalPermissionRoles"; import type { History } from "history"; import { connect } from "react-redux"; import { compose } from "redux"; +import type { Links } from "@scm-manager/ui-types"; +import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; import { getLinks } from "../../modules/indexResource"; +import GlobalConfig from "./GlobalConfig"; +import GlobalPermissionRoles from "./GlobalPermissionRoles"; +import GlobalPermissionRoleForm from "./GlobalPermissionRoleForm"; type Props = { links: Links, @@ -34,6 +34,12 @@ class Config extends React.Component<Props> { return this.stripEndingSlash(this.props.match.url); }; + matchesRoles = (route: any) => { + const url = this.matchedUrl(); + const regex = new RegExp(`${url}/role/.+/edit`); + return route.location.pathname.match(regex); + }; + render() { const { links, t } = this.props; @@ -53,10 +59,11 @@ class Config extends React.Component<Props> { exact render={() => ( <GlobalPermissionRoles - baseUrl={`${url}/roles`} + baseUrl={`${url}/role`} /> )} /> + <Route path={`${url}/role`} component={GlobalPermissionRoleForm} /> <ExtensionPoint name="config.route" props={extensionProps} @@ -73,6 +80,7 @@ class Config extends React.Component<Props> { <NavLink to={`${url}/roles`} label={t("roles.navLink")} + activeWhenMatch={this.matchesRoles} /> <ExtensionPoint name="config.navigation" From 10d86391c2b600f2b1decafe093b1acb22ebe736 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:57:51 +0200 Subject: [PATCH 45/91] renamed table link direction from 'info' to 'edit' --- scm-ui/src/config/components/table/PermissionRoleRow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/components/table/PermissionRoleRow.js b/scm-ui/src/config/components/table/PermissionRoleRow.js index a47d7e7e27..f42627540c 100644 --- a/scm-ui/src/config/components/table/PermissionRoleRow.js +++ b/scm-ui/src/config/components/table/PermissionRoleRow.js @@ -15,7 +15,7 @@ class PermissionRoleRow extends React.Component<Props> { render() { const { baseUrl, role } = this.props; - const to = `${baseUrl}/${encodeURIComponent(role.name)}/info`; + const to = `${baseUrl}/${encodeURIComponent(role.name)}/edit`; return ( <tr> <td>{this.renderLink(to, role.name)}</td> From 47c81dbc6a73566014bbbbb4ec678c2370c43c58 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 16:59:32 +0200 Subject: [PATCH 46/91] fixed trans --- scm-ui/src/config/components/table/PermissionRoleTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/components/table/PermissionRoleTable.js b/scm-ui/src/config/components/table/PermissionRoleTable.js index 3c9dde1cc5..895883d8c4 100644 --- a/scm-ui/src/config/components/table/PermissionRoleTable.js +++ b/scm-ui/src/config/components/table/PermissionRoleTable.js @@ -18,7 +18,7 @@ class PermissionRoleTable extends React.Component<Props> { <table className="card-table table is-hoverable is-fullwidth"> <thead> <tr> - <th>{t("role.form.name")}</th> + <th>{t("roles.form.name")}</th> </tr> </thead> <tbody> From 79311a5947c372a43daffa5eb68db7d3fd5d66e0 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 9 May 2019 17:12:08 +0200 Subject: [PATCH 47/91] fixed tests --- scm-ui/src/config/modules/roles.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/modules/roles.test.js index e9a12bc30a..fc165f863a 100644 --- a/scm-ui/src/config/modules/roles.test.js +++ b/scm-ui/src/config/modules/roles.test.js @@ -320,7 +320,7 @@ describe("repository roles reducer", () => { describe("repository roles selector", () => { it("should return an empty object", () => { expect(selectListAsCollection({})).toEqual({}); - expect(selectListAsCollection({ repositoryRoles: { a: "a" } })).toEqual({}); + expect(selectListAsCollection({ roles: { a: "a" } })).toEqual({}); }); it("should return a state slice collection", () => { @@ -330,7 +330,7 @@ describe("repository roles selector", () => { }; const state = { - repositoryRoles: { + roles: { list: { entry: collection } @@ -342,18 +342,18 @@ describe("repository roles selector", () => { it("should return false", () => { expect(isPermittedToCreateRoles({})).toBe(false); expect( - isPermittedToCreateRoles({ repositoryRoles: { list: { entry: {} } } }) + isPermittedToCreateRoles({ roles: { list: { entry: {} } } }) ).toBe(false); expect( isPermittedToCreateRoles({ - repositoryRoles: { list: { entry: { roleCreatePermission: false } } } + roles: { list: { entry: { roleCreatePermission: false } } } }) ).toBe(false); }); it("should return true", () => { const state = { - repositoryRoles: { + roles: { list: { entry: { roleCreatePermission: true @@ -366,7 +366,7 @@ describe("repository roles selector", () => { it("should get repositoryRoles from state", () => { const state = { - repositoryRoles: { + roles: { list: { entries: ["a", "b"] }, @@ -438,7 +438,7 @@ describe("repository roles selector", () => { it("should return role1", () => { const state = { - repositoryRoles: { + roles: { byNames: { role1: role1 } From 163cdf7a6bd2cc64b5e817d1234bee3f5f69f8e9 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 10 May 2019 09:28:17 +0200 Subject: [PATCH 48/91] update RepositoryRole-Model --- .../java/sonia/scm/api/v2/resources/RepositoryRoleDto.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java index b81789c110..7840cad0ee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.validator.constraints.NotEmpty; +import java.time.Instant; import java.util.Collection; @Getter @@ -18,7 +19,9 @@ public class RepositoryRoleDto extends HalRepresentation { private String name; @NoBlankStrings @NotEmpty private Collection<String> verbs; - private boolean system; + private String type; + private Instant creationDate; + private Instant lastModified; RepositoryRoleDto(Links links, Embedded embedded) { super(links, embedded); From a26f86d545ba6f5b0c4796e721a200720391a81d Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 10 May 2019 09:29:38 +0200 Subject: [PATCH 49/91] fix repositoryRole systemrole check --- .../resources/RepositoryRoleToRepositoryRoleDtoMapper.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java index 5f77e38fed..43d364c1b3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java @@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.mapstruct.Mapper; -import org.mapstruct.Mapping; import org.mapstruct.ObjectFactory; import sonia.scm.repository.RepositoryRole; import sonia.scm.repository.RepositoryRolePermissions; @@ -23,13 +22,12 @@ public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper private ResourceLinks resourceLinks; @Override - @Mapping(source = "type", target = "system") public abstract RepositoryRoleDto map(RepositoryRole modelObject); @ObjectFactory RepositoryRoleDto createDto(RepositoryRole repositoryRole) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName())); - if (!isSystemRole(repositoryRole.getType()) && RepositoryRolePermissions.modify().isPermitted()) { + if (!"system".equals(repositoryRole.getType()) && RepositoryRolePermissions.modify().isPermitted()) { linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName()))); linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName()))); } @@ -40,7 +38,4 @@ public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper return new RepositoryRoleDto(linksBuilder.build(), embeddedBuilder.build()); } - boolean isSystemRole(String type) { - return "system".equals(type); - } } From 7fa2898420c0b1e62389fb8406ebb50558ef30d4 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 10 May 2019 13:23:01 +0200 Subject: [PATCH 50/91] throw Exception when modify repo-role type --- scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java index e89cedb750..f338d6d277 100644 --- a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java @@ -7,6 +7,8 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; + public class ManagerDaoAdapter<T extends ModelObject> { private final GenericDAO<T> dao; @@ -19,6 +21,9 @@ public class ManagerDaoAdapter<T extends ModelObject> { T notModified = dao.get(object.getId()); if (notModified != null) { permissionCheck.apply(notModified).check(); + + doThrow().violation("type must not be changed").when(!notModified.getType().equals(object.getType())); + AssertUtil.assertIsValid(object); beforeUpdate.handle(notModified); From e0ab70591adeca28e9c6514c584321e188aae0ec Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 10 May 2019 13:23:41 +0200 Subject: [PATCH 51/91] fix Tests --- .../RepositoryRoleRootResourceTest.java | 22 +++++++++---------- .../DefaultRepositoryRoleManagerTest.java | 9 +++++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java index 0489717926..af968efd1f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java @@ -117,9 +117,9 @@ public class RepositoryRoleRootResourceTest { .contains( "\"name\":\"" + CUSTOM_ROLE + "\"", "\"verbs\":[\"verb\"]", - "\"self\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" + "\"self\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}" ); } @@ -135,11 +135,11 @@ public class RepositoryRoleRootResourceTest { .contains( "\"name\":\"" + SYSTEM_ROLE + "\"", "\"verbs\":[\"admin\"]", - "\"self\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + "\"self\":{\"href\":\"/v2/repositoryRoles/" + SYSTEM_ROLE + "\"}" ) .doesNotContain( - "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}" ); } @@ -215,7 +215,7 @@ public class RepositoryRoleRootResourceTest { assertThat(createCaptor.getValue().getName()).isEqualTo("newRole"); assertThat(createCaptor.getValue().getVerbs()).containsExactly("write", "push"); Object location = response.getOutputHeaders().getFirst("Location"); - assertThat(location).isEqualTo(create("/v2/repository-roles/newRole")); + assertThat(location).isEqualTo(create("/v2/repositoryRoles/newRole")); } @Test @@ -245,12 +245,12 @@ public class RepositoryRoleRootResourceTest { "\"name\":\"" + SYSTEM_ROLE + "\"", "\"verbs\":[\"verb\"]", "\"verbs\":[\"admin\"]", - "\"self\":{\"href\":\"/v2/repository-roles", - "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", - "\"create\":{\"href\":\"/v2/repository-roles/\"}" + "\"self\":{\"href\":\"/v2/repositoryRoles", + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}", + "\"create\":{\"href\":\"/v2/repositoryRoles/\"}" ) .doesNotContain( - "\"delete\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + "\"delete\":{\"href\":\"/v2/repositoryRoles/" + SYSTEM_ROLE + "\"}" ); } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java index 0542626dd5..cd0ba963b9 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java @@ -16,6 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import sonia.scm.NotFoundException; +import sonia.scm.ScmConstraintViolationException; import sonia.scm.security.RepositoryPermissionProvider; import java.util.Collection; @@ -119,11 +120,17 @@ class DefaultRepositoryRoleManagerTest { @Test void shouldModifyRole() { - RepositoryRole role = new RepositoryRole(CUSTOM_ROLE_NAME, singletonList("changed"), null); + RepositoryRole role = new RepositoryRole(CUSTOM_ROLE_NAME, singletonList("changed"), "xml"); manager.modify(role); verify(dao).modify(role); } + @Test + void shouldNotModifyRole_whenTypeChanged() { + assertThrows(ScmConstraintViolationException.class, () -> manager.modify(new RepositoryRole(CUSTOM_ROLE_NAME, singletonList("changed"), null))); + verify(dao, never()).modify(any()); + } + @Test void shouldNotModifyRole_whenRoleDoesNotExists() { assertThrows(NotFoundException.class, () -> manager.modify(new RepositoryRole("noSuchRole", singletonList("changed"), null))); From fad50b7319c556956fa5f8c07568e2e82821f18a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 13 May 2019 15:42:35 +0200 Subject: [PATCH 52/91] update RepositoryRole type --- .../ui-types/src/AvailableRepositoryPermissions.js | 11 ----------- .../packages/ui-types/src/RepositoryRole.js | 10 ++++++++++ scm-ui-components/packages/ui-types/src/index.js | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js create mode 100644 scm-ui-components/packages/ui-types/src/RepositoryRole.js diff --git a/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js deleted file mode 100644 index ab7e8d82e4..0000000000 --- a/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow - -export type RepositoryRole = { - name: string, - verbs: string[] -}; - -export type AvailableRepositoryPermissions = { - availableVerbs: string[], - availableRoles: RepositoryRole[] -}; diff --git a/scm-ui-components/packages/ui-types/src/RepositoryRole.js b/scm-ui-components/packages/ui-types/src/RepositoryRole.js new file mode 100644 index 0000000000..8b8ffc933f --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/RepositoryRole.js @@ -0,0 +1,10 @@ +// @flow + +export type RepositoryRole = { + name: string, + verbs: string[], + type?: string, + creationDate?: string, + lastModified?: string, +}; + diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index 59c423c5d5..0272b1e2cb 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -26,6 +26,6 @@ export type { SubRepository, File } from "./Sources"; export type { SelectValue, AutocompleteObject } from "./Autocomplete"; -export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions"; +export type { RepositoryRole } from "./RepositoryRole"; export type { NamespaceStrategies } from "./NamespaceStrategies"; From 623e32809bdedb0f4ed17985db9190b5fae925c6 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 13 May 2019 15:44:43 +0200 Subject: [PATCH 53/91] fix fetching available_repository_roles & available_repository_verbs --- scm-ui/src/modules/indexResource.js | 8 ++++ .../containers/CreatePermissionForm.js | 19 ++++---- .../permissions/containers/Permissions.js | 40 +++++++++++----- .../containers/SinglePermission.js | 25 +++++----- .../repos/permissions/modules/permissions.js | 48 +++++++++++++++---- 5 files changed, 99 insertions(+), 41 deletions(-) diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index 0facb51faa..2c6805d207 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -127,6 +127,14 @@ export function getUsersLink(state: Object) { return getLink(state, "users"); } +export function getRepositoryRolesLink(state: Object) { + return getLink(state, "repositoryRoles"); +} + +export function getRepositoryVerbsLink(state: Object) { + return getLink(state, "repositoryVerbs"); +} + export function getGroupsLink(state: Object) { return getLink(state, "groups"); } diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index d2300b9864..94fc707dc1 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -10,7 +10,7 @@ import { } from "@scm-manager/ui-components"; import RoleSelector from "../components/RoleSelector"; import type { - AvailableRepositoryPermissions, + RepositoryRole, PermissionCollection, PermissionCreateEntry, SelectValue @@ -21,7 +21,8 @@ import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; type Props = { t: string => string, - availablePermissions: AvailableRepositoryPermissions, + availableRoles: RepositoryRole[], + availableVerbs: string[], createPermission: (permission: PermissionCreateEntry) => void, loading: boolean, currentPermissions: PermissionCollection, @@ -44,7 +45,7 @@ class CreatePermissionForm extends React.Component<Props, State> { this.state = { name: "", - verbs: props.availablePermissions.availableRoles[0].verbs, + verbs: props.availableRoles[0].verbs, groupPermission: false, valid: true, value: undefined, @@ -132,18 +133,18 @@ class CreatePermissionForm extends React.Component<Props, State> { }; render() { - const { t, availablePermissions, loading } = this.props; + const { t, availableRoles, availableVerbs, loading } = this.props; const { verbs, showAdvancedDialog } = this.state; - const availableRoleNames = availablePermissions.availableRoles.map( + const availableRoleNames = availableRoles.map( r => r.name ); - const matchingRole = findMatchingRoleName(availablePermissions, verbs); + const matchingRole = findMatchingRoleName(availableRoles, verbs); const advancedDialog = showAdvancedDialog ? ( <AdvancedPermissionsDialog - availableVerbs={availablePermissions.availableVerbs} + availableVerbs={availableVerbs} selectedVerbs={verbs} onClose={this.closeAdvancedPermissionsDialog} onSubmit={this.submitAdvancedPermissionsDialog} @@ -246,7 +247,7 @@ class CreatePermissionForm extends React.Component<Props, State> { removeState = () => { this.setState({ name: "", - verbs: this.props.availablePermissions.availableRoles[0].verbs, + verbs: this.props.availableRoles[0].verbs, valid: true, value: undefined }); @@ -263,7 +264,7 @@ class CreatePermissionForm extends React.Component<Props, State> { }; findAvailableRole = (roleName: string) => { - return this.props.availablePermissions.availableRoles.find( + return this.props.availableRoles.find( role => role.name === roleName ); }; diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 6fd9cbab38..02c43d254c 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -19,7 +19,7 @@ import { getDeletePermissionsFailure, getModifyPermissionsFailure, modifyPermissionReset, - deletePermissionReset + deletePermissionReset, getAvailableRepositoryRoles, getAvailableRepositoryVerbs } from "../modules/permissions"; import { Loading, @@ -28,22 +28,23 @@ import { LabelWithHelpIcon } from "@scm-manager/ui-components"; import type { - AvailableRepositoryPermissions, Permission, PermissionCollection, - PermissionCreateEntry + PermissionCreateEntry, + RepositoryRole } from "@scm-manager/ui-types"; import SinglePermission from "./SinglePermission"; import CreatePermissionForm from "./CreatePermissionForm"; import type { History } from "history"; import { getPermissionsLink } from "../../modules/repos"; import { - getGroupAutoCompleteLink, + getGroupAutoCompleteLink, getRepositoryRolesLink, getRepositoryVerbsLink, getUserAutoCompleteLink } from "../../../modules/indexResource"; type Props = { - availablePermissions: AvailableRepositoryPermissions, + availableRepositoryRoles: RepositoryRole[], + availableVerbs: string[], namespace: string, repoName: string, loading: boolean, @@ -51,6 +52,8 @@ type Props = { permissions: PermissionCollection, hasPermissionToCreate: boolean, loadingCreatePermission: boolean, + repositoryRolesLink: string, + repositoryVerbsLink: string, permissionsLink: string, groupAutoCompleteLink: string, userAutoCompleteLink: string, @@ -85,13 +88,15 @@ class Permissions extends React.Component<Props> { modifyPermissionReset, createPermissionReset, deletePermissionReset, - permissionsLink + permissionsLink, + repositoryRolesLink, + repositoryVerbsLink } = this.props; createPermissionReset(namespace, repoName); modifyPermissionReset(namespace, repoName); deletePermissionReset(namespace, repoName); - fetchAvailablePermissionsIfNeeded(); + fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink); fetchPermissions(permissionsLink, namespace, repoName); } @@ -107,6 +112,8 @@ class Permissions extends React.Component<Props> { render() { const { availablePermissions, + availableRepositoryRoles, + availableVerbs, loading, error, permissions, @@ -134,7 +141,8 @@ class Permissions extends React.Component<Props> { const createPermissionForm = hasPermissionToCreate ? ( <CreatePermissionForm - availablePermissions={availablePermissions} + availableRoles={availableRepositoryRoles} + availableVerbs={availableVerbs} createPermission={permission => this.createPermission(permission)} loading={loadingCreatePermission} currentPermissions={permissions} @@ -174,7 +182,8 @@ class Permissions extends React.Component<Props> { {permissions.map(permission => { return ( <SinglePermission - availablePermissions={availablePermissions} + availableRepositoryRoles={availableRepositoryRoles} + availableRepositoryVerbs={availableVerbs} key={permission.name + permission.groupPermission.toString()} namespace={namespace} repoName={repoName} @@ -209,14 +218,23 @@ const mapStateToProps = (state, ownProps) => { repoName ); const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); + const repositoryRolesLink = getRepositoryRolesLink(state); + const repositoryVerbsLink = getRepositoryVerbsLink(state); const permissionsLink = getPermissionsLink(state, namespace, repoName); const groupAutoCompleteLink = getGroupAutoCompleteLink(state); const userAutoCompleteLink = getUserAutoCompleteLink(state); const availablePermissions = getAvailablePermissions(state); + const availableRepositoryRoles = getAvailableRepositoryRoles(state); + const availableVerbs = getAvailableRepositoryVerbs(state); + return { availablePermissions, + availableRepositoryRoles, + availableVerbs, namespace, repoName, + repositoryRolesLink, + repositoryVerbsLink, error, loading, permissions, @@ -233,8 +251,8 @@ const mapDispatchToProps = dispatch => { fetchPermissions: (link: string, namespace: string, repoName: string) => { dispatch(fetchPermissions(link, namespace, repoName)); }, - fetchAvailablePermissionsIfNeeded: () => { - dispatch(fetchAvailablePermissionsIfNeeded()); + fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => { + dispatch(fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink)); }, createPermission: ( link: string, diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js index e5a9c604a6..c90d6ab968 100644 --- a/scm-ui/src/repos/permissions/containers/SinglePermission.js +++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js @@ -1,7 +1,7 @@ // @flow import React from "react"; import type { - AvailableRepositoryPermissions, + RepositoryRole, Permission } from "@scm-manager/ui-types"; import { translate } from "react-i18next"; @@ -22,7 +22,8 @@ import classNames from "classnames"; import injectSheet from "react-jss"; type Props = { - availablePermissions: AvailableRepositoryPermissions, + availableRepositoryRoles: RepositoryRole[], + availableRepositoryVerbs: string[], submitForm: Permission => void, modifyPermission: ( permission: Permission, @@ -68,8 +69,8 @@ class SinglePermission extends React.Component<Props, State> { constructor(props: Props) { super(props); - const defaultPermission = props.availablePermissions.availableRoles - ? props.availablePermissions.availableRoles[0] + const defaultPermission = props.availableRoles + ? props.availableRoles[0] : {}; this.state = { @@ -85,10 +86,10 @@ class SinglePermission extends React.Component<Props, State> { } componentDidMount() { - const { availablePermissions, permission } = this.props; + const { availableRepositoryRoles, permission } = this.props; const matchingRole = findMatchingRoleName( - availablePermissions, + availableRepositoryRoles, permission.verbs ); @@ -117,13 +118,14 @@ class SinglePermission extends React.Component<Props, State> { const { role, permission, showAdvancedDialog } = this.state; const { t, - availablePermissions, + availableRepositoryRoles, + availableRepositoryVerbs, loading, namespace, repoName, classes } = this.props; - const availableRoleNames = availablePermissions.availableRoles.map( + const availableRoleNames = !!availableRepositoryRoles && availableRepositoryRoles.map( r => r.name ); const readOnly = !this.mayChangePermissions(); @@ -143,7 +145,7 @@ class SinglePermission extends React.Component<Props, State> { const advancedDialg = showAdvancedDialog ? ( <AdvancedPermissionsDialog readOnly={readOnly} - availableVerbs={availablePermissions.availableVerbs} + availableVerbs={availableRepositoryVerbs} selectedVerbs={permission.verbs} onClose={this.closeAdvancedPermissionsDialog} onSubmit={this.submitAdvancedPermissionsDialog} @@ -198,7 +200,7 @@ class SinglePermission extends React.Component<Props, State> { submitAdvancedPermissionsDialog = (newVerbs: string[]) => { const { permission } = this.state; const newRole = findMatchingRoleName( - this.props.availablePermissions, + this.props.availableRoles, newVerbs ); this.setState( @@ -226,7 +228,8 @@ class SinglePermission extends React.Component<Props, State> { }; findAvailableRole = (roleName: string) => { - return this.props.availablePermissions.availableRoles.find( + const { availableRepositoryRoles } = this.props; + return availableRepositoryRoles.find( role => role.name === roleName ); }; diff --git a/scm-ui/src/repos/permissions/modules/permissions.js b/scm-ui/src/repos/permissions/modules/permissions.js index 8c4161e907..2b0263bf35 100644 --- a/scm-ui/src/repos/permissions/modules/permissions.js +++ b/scm-ui/src/repos/permissions/modules/permissions.js @@ -4,7 +4,7 @@ import type { Action } from "@scm-manager/ui-components"; import { apiClient } from "@scm-manager/ui-components"; import * as types from "../../../modules/types"; import type { - AvailableRepositoryPermissions, + RepositoryRole, Permission, PermissionCollection, PermissionCreateEntry @@ -12,7 +12,6 @@ import type { import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; import { Dispatch } from "redux"; -import { getLinks } from "../../../modules/indexResource"; export const FETCH_AVAILABLE = "scm/permissions/FETCH_AVAILABLE"; export const FETCH_AVAILABLE_PENDING = `${FETCH_AVAILABLE}_${ @@ -78,22 +77,36 @@ const CONTENT_TYPE = "application/vnd.scmm-repositoryPermission+json"; // fetch available permissions -export function fetchAvailablePermissionsIfNeeded() { +export function fetchAvailablePermissionsIfNeeded(repositoryRolesLink: string, repositoryVerbsLink: string) { return function(dispatch: any, getState: () => Object) { if (shouldFetchAvailablePermissions(getState())) { - return fetchAvailablePermissions(dispatch, getState); + return fetchAvailablePermissions(dispatch, getState, repositoryRolesLink, repositoryVerbsLink); } }; } export function fetchAvailablePermissions( dispatch: any, - getState: () => Object + getState: () => Object, + repositoryRolesLink: string, + repositoryVerbsLink: string ) { dispatch(fetchAvailablePending()); return apiClient - .get(getLinks(getState()).availableRepositoryPermissions.href) - .then(response => response.json()) + .get(repositoryRolesLink) + .then(repositoryRoles => repositoryRoles.json()) + .then(repositoryRoles => repositoryRoles._embedded.repositoryRoles) + .then(repositoryRoles => { + return apiClient.get(repositoryVerbsLink) + .then(repositoryVerbs => repositoryVerbs.json()) + .then(repositoryVerbs => repositoryVerbs.verbs) + .then(repositoryVerbs => { + return { + repositoryVerbs, + repositoryRoles + }; + }); + }) .then(available => { dispatch(fetchAvailableSuccess(available)); }) @@ -121,7 +134,7 @@ export function fetchAvailablePending(): Action { } export function fetchAvailableSuccess( - available: AvailableRepositoryPermissions + available: [RepositoryRole[], string[]] ): Action { return { type: FETCH_AVAILABLE_SUCCESS, @@ -543,6 +556,21 @@ export function getAvailablePermissions(state: Object) { } } +export function getAvailableRepositoryRoles(state: Object) { + return available(state).repositoryRoles; +} + +export function getAvailableRepositoryVerbs(state: Object) { + return available(state).repositoryVerbs; +} + +function available(state: Object) { + if (state.permissions && state.permissions.available) { + return state.permissions.available; + } + return {}; +} + export function getPermissionsOfRepo( state: Object, namespace: string, @@ -705,13 +733,13 @@ export function getModifyPermissionsFailure( } export function findMatchingRoleName( - availablePermissions: AvailableRepositoryPermissions, + availableRoles: RepositoryRole[], verbs: string[] ) { if (!verbs) { return ""; } - const matchingRole = availablePermissions.availableRoles.find(role => { + const matchingRole = !! availableRoles && availableRoles.find(role => { return equalVerbs(role.verbs, verbs); }); From 3c526a8ac2955291a22171a352408559df32d587 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Tue, 14 May 2019 16:56:35 +0200 Subject: [PATCH 54/91] prepare GlobalPermissionRoleForm / fetch AvailableVerbs from State --- .../containers/GlobalPermissionRoleForm.js | 150 +++++++++++++++++- scm-ui/src/config/modules/roles.js | 73 ++++++++- 2 files changed, 218 insertions(+), 5 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js index ba6cd293db..3fb7d539f7 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js @@ -1,14 +1,156 @@ // @flow import React from "react"; +import { InputField, SubmitButton } from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; +import PermissionCheckbox from "../../repos/permissions/components/PermissionCheckbox"; +import { + fetchAvailableVerbs, + getFetchVerbsFailure, + getVerbsFromState, + isFetchVerbsPending +} from "../modules/roles"; +import { getRepositoryVerbsLink } from "../../modules/indexResource"; +import { connect } from "react-redux"; type Props = { - thing?: any, + submitForm: CustomRoleRequest => void, + transmittedName?: string, + loading?: boolean, + availableVerbs: string[], + selectedVerbs: string[], + verbsLink: string, + t: string => string, + + // dispatch functions + fetchAvailableVerbs: (link: string) => void }; -class GlobalPermissionRoleForm extends React.Component<Props> { +type State = { + name?: string, + verbs: string[] +}; + +class GlobalPermissionRoleForm extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + name: props.transmittedName ? props.transmittedName : "", + verbs: props.availableVerbs + }; + } + + componentWillMount() { + const { fetchAvailableVerbs, verbsLink } = this.props; + fetchAvailableVerbs(verbsLink); + } + + componentDidMount() { + const { + fetchAvailableVerbs, + verbsLink, + } = this.props; + fetchAvailableVerbs(verbsLink); + + } + + isValid = () => { + const { name, verbs } = this.state; + return !(this.isFalsy(name) || this.isFalsy(verbs) || verbs.isEmpty()); + }; + + isFalsy(value) { + return !value; + } + + handleNameChange = (name: string) => { + this.setState({ + ...this.state, + name + }); + }; + + handleVerbChange = (value: boolean, name: string) => { + const { selectedVerbs } = this.state; + const newVerbs = { ...selectedVerbs, [name]: value }; + this.setState({ selectedVerbs: newVerbs }); + }; + render() { - return <p>Placeholder</p>; + const { + t, + transmittedName, + loading, + disabled, + availableVerbs + } = this.props; + const { verbs } = this.state; + + const verbSelectBoxes = + !!availableVerbs && + Object.entries(availableVerbs).map(e => ( + <PermissionCheckbox + key={e[0]} + // disabled={readOnly} + name={e[0]} + checked={e[1]} + onChange={this.handleVerbChange} + /> + )); + + return ( + <div> + <form onSubmit={this.submit}> + <div className="columns"> + <div className="column"> + <InputField + name="name" + label={t("roles.create.name")} + onChange={this.handleNameChange} + value={name ? name : ""} + disabled={!!transmittedName || disabled} + /> + </div> + </div> + <>{verbSelectBoxes}</> + <div className="columns"> + <div className="column"> + <SubmitButton + disabled={disabled || !this.isValid()} + loading={loading} + label={t("roles.create.submit")} + /> + </div> + </div> + </form> + </div> + ); } } -export default GlobalPermissionRoleForm; +const mapStateToProps = (state, ownProps) => { + const loading = isFetchVerbsPending(state); + const error = getFetchVerbsFailure(state); + const verbsLink = getRepositoryVerbsLink(state); + const availableVerbs = getVerbsFromState(state); + + return { + loading, + error, + verbsLink, + availableVerbs + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchAvailableVerbs: (link: string) => { + dispatch(fetchAvailableVerbs(link)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("roles")(GlobalPermissionRoleForm)); diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index c241ca15e9..eea062a6b9 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -16,6 +16,11 @@ export const FETCH_ROLE_PENDING = `${FETCH_ROLE}_${types.PENDING_SUFFIX}`; export const FETCH_ROLE_SUCCESS = `${FETCH_ROLE}_${types.SUCCESS_SUFFIX}`; export const FETCH_ROLE_FAILURE = `${FETCH_ROLE}_${types.FAILURE_SUFFIX}`; +export const FETCH_VERBS = "scm/roles/FETCH_VERBS"; +export const FETCH_VERBS_PENDING = `${FETCH_VERBS}_${types.PENDING_SUFFIX}`; +export const FETCH_VERBS_SUCCESS = `${FETCH_VERBS}_${types.SUCCESS_SUFFIX}`; +export const FETCH_VERBS_FAILURE = `${FETCH_VERBS}_${types.FAILURE_SUFFIX}`; + export const CREATE_ROLE = "scm/roles/CREATE_ROLE"; export const CREATE_ROLE_PENDING = `${CREATE_ROLE}_${types.PENDING_SUFFIX}`; export const CREATE_ROLE_SUCCESS = `${CREATE_ROLE}_${types.SUCCESS_SUFFIX}`; @@ -173,6 +178,59 @@ export function createRole(link: string, role: Role, callback?: () => void) { }; } +//fetch verbs +export function fetchVerbsPending(): Action { + return { + type: FETCH_VERBS_PENDING + }; +} + +export function fetchVerbsSuccess(verbs: any): Action { + return { + type: FETCH_VERBS_SUCCESS, + payload: verbs + }; +} + +export function fetchVerbsFailure(error: Error): Action { + return { + type: FETCH_VERBS_FAILURE, + payload: error + }; +} + +export function fetchAvailableVerbs(link: string) { + return function(dispatch: any) { + dispatch(fetchVerbsPending()); + return apiClient + .get(link) + .then(response => { + return response.json(); + }) + .then(data => { + dispatch(fetchVerbsSuccess(data)); + }) + .catch(error => { + dispatch(fetchVerbsFailure(error)); + }); + }; +} + +function verbReducer(state: any = {}, action: any = {}) { + switch (action.type) { + case FETCH_VERBS_SUCCESS: + const verbs = action.payload.verbs; + const verbMap = {}; + verbs.forEach(p => (verbMap[p] = false)); + return { + ...state, + verbMap + }; + default: + return state; + } +} + function listReducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_ROLES_SUCCESS: @@ -237,7 +295,8 @@ function byNamesReducer(state: any = {}, action: any = {}) { export default combineReducers({ list: listReducer, - byNames: byNamesReducer + byNames: byNamesReducer, + verbs: verbReducer }); // selectors @@ -278,6 +337,10 @@ export function getRolesFromState(state: Object) { return roleEntries; } +export function getVerbsFromState(state: Object) { + return state.roles.verbs.verbs +} + export function isFetchRolesPending(state: Object) { return isPending(state, FETCH_ROLES); } @@ -294,6 +357,14 @@ export function getCreateRoleFailure(state: Object) { return getFailure(state, CREATE_ROLE); } +export function isFetchVerbsPending(state: Object) { + return isPending(state, FETCH_VERBS); +} + +export function getFetchVerbsFailure(state: Object) { + return getFailure(state, FETCH_VERBS); +} + export function getRoleByName(state: Object, name: string) { if (state.roles && state.roles.byNames) { return state.roles.byNames[name]; From 6969003a4cc57bc9e6bb3fa900538b064292adc9 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 02:04:54 +0200 Subject: [PATCH 55/91] refactor --- .../containers/GlobalPermissionRoleForm.js | 109 +++++++++--------- .../containers/GlobalPermissionRoles.js | 16 +-- scm-ui/src/config/modules/roles.js | 4 +- 3 files changed, 65 insertions(+), 64 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js index 3fb7d539f7..f92a212d20 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js @@ -1,7 +1,9 @@ // @flow import React from "react"; -import { InputField, SubmitButton } from "@scm-manager/ui-components"; +import { connect } from "react-redux"; import { translate } from "react-i18next"; +import type { Role } from "@scm-manager/ui-types"; +import { InputField, SubmitButton } from "@scm-manager/ui-components"; import PermissionCheckbox from "../../repos/permissions/components/PermissionCheckbox"; import { fetchAvailableVerbs, @@ -10,15 +12,16 @@ import { isFetchVerbsPending } from "../modules/roles"; import { getRepositoryVerbsLink } from "../../modules/indexResource"; -import { connect } from "react-redux"; type Props = { submitForm: CustomRoleRequest => void, - transmittedName?: string, + role?: Role, loading?: boolean, availableVerbs: string[], selectedVerbs: string[], verbsLink: string, + + // context objects t: string => string, // dispatch functions @@ -26,7 +29,7 @@ type Props = { }; type State = { - name?: string, + role: Role, verbs: string[] }; @@ -35,56 +38,56 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { super(props); this.state = { - name: props.transmittedName ? props.transmittedName : "", + role: { + name: "", + verbs: [], + system: false, + _links: {} + }, verbs: props.availableVerbs }; } - componentWillMount() { - const { fetchAvailableVerbs, verbsLink } = this.props; - fetchAvailableVerbs(verbsLink); - } - componentDidMount() { - const { - fetchAvailableVerbs, - verbsLink, - } = this.props; + const { fetchAvailableVerbs, verbsLink, role } = this.props; fetchAvailableVerbs(verbsLink); + if (role) { + this.setState({ role: { ...role } }); + } } - isValid = () => { - const { name, verbs } = this.state; - return !(this.isFalsy(name) || this.isFalsy(verbs) || verbs.isEmpty()); - }; - isFalsy(value) { return !value; } + isValid = () => { + const { role } = this.state; + return !( + this.isFalsy(role) || + this.isFalsy(role.name) || + this.isFalsy(role.verbs) + ); + }; + handleNameChange = (name: string) => { this.setState({ - ...this.state, - name + role: { + ...this.state.role, + name + } }); }; handleVerbChange = (value: boolean, name: string) => { - const { selectedVerbs } = this.state; + const { selectedVerbs } = this.props; const newVerbs = { ...selectedVerbs, [name]: value }; - this.setState({ selectedVerbs: newVerbs }); + this.setState({ verbs: newVerbs }); }; render() { - const { - t, - transmittedName, - loading, - disabled, - availableVerbs - } = this.props; - const { verbs } = this.state; + const { loading, availableVerbs, t } = this.props; + const { role, verbs } = this.state; const verbSelectBoxes = !!availableVerbs && @@ -99,31 +102,29 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { )); return ( - <div> - <form onSubmit={this.submit}> - <div className="columns"> - <div className="column"> - <InputField - name="name" - label={t("roles.create.name")} - onChange={this.handleNameChange} - value={name ? name : ""} - disabled={!!transmittedName || disabled} - /> - </div> + <form onSubmit={this.submit}> + <div className="columns"> + <div className="column"> + <InputField + name="name" + label={t("roles.create.name")} + onChange={this.handleNameChange} + value={role.name ? role.name : ""} + disabled={!!role.name} // || disabled + /> </div> - <>{verbSelectBoxes}</> - <div className="columns"> - <div className="column"> - <SubmitButton - disabled={disabled || !this.isValid()} - loading={loading} - label={t("roles.create.submit")} - /> - </div> + </div> + <>{verbSelectBoxes}</> + <div className="columns"> + <div className="column"> + <SubmitButton + loading={loading} + label={t("roles.create.submit")} + //disabled={disabled || !this.isValid()} + /> </div> - </form> - </div> + </div> + </form> ); } } diff --git a/scm-ui/src/config/containers/GlobalPermissionRoles.js b/scm-ui/src/config/containers/GlobalPermissionRoles.js index eb579f6e69..2691bf3988 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoles.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoles.js @@ -5,14 +5,6 @@ import {withRouter} from "react-router-dom"; import { translate } from "react-i18next"; import type { History } from "history"; import type { Role, PagedCollection } from "@scm-manager/ui-types"; -import { - fetchRolesByPage, - getRolesFromState, - selectListAsCollection, - isPermittedToCreateRoles, - isFetchRolesPending, - getFetchRolesFailure -} from "../modules/roles"; import { Title, Loading, @@ -21,6 +13,14 @@ import { urls, CreateButton } from "@scm-manager/ui-components"; +import { + fetchRolesByPage, + getRolesFromState, + selectListAsCollection, + isPermittedToCreateRoles, + isFetchRolesPending, + getFetchRolesFailure +} from "../modules/roles"; import PermissionRoleTable from "../components/table/PermissionRoleTable"; import { getRolesLink } from "../../modules/indexResource"; type Props = { diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index eea062a6b9..7088d46fb8 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -225,7 +225,7 @@ function verbReducer(state: any = {}, action: any = {}) { return { ...state, verbMap - }; + }; default: return state; } @@ -338,7 +338,7 @@ export function getRolesFromState(state: Object) { } export function getVerbsFromState(state: Object) { - return state.roles.verbs.verbs + return state.roles.verbs.verbs; } export function isFetchRolesPending(state: Object) { From 4388efc2afac7c4559e3ec0d35d4cff768e3201c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 15 May 2019 09:31:03 +0200 Subject: [PATCH 56/91] fix fetch available verbs and map to state --- .../containers/GlobalPermissionRoleForm.js | 71 ++++++++++++------- scm-ui/src/config/modules/roles.js | 7 +- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js index f92a212d20..77e743d22b 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js @@ -14,11 +14,10 @@ import { import { getRepositoryVerbsLink } from "../../modules/indexResource"; type Props = { - submitForm: CustomRoleRequest => void, role?: Role, loading?: boolean, + disabled: boolean, availableVerbs: string[], - selectedVerbs: string[], verbsLink: string, // context objects @@ -29,8 +28,7 @@ type Props = { }; type State = { - role: Role, - verbs: string[] + role: Role }; class GlobalPermissionRoleForm extends React.Component<Props, State> { @@ -43,8 +41,7 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { verbs: [], system: false, _links: {} - }, - verbs: props.availableVerbs + } }; } @@ -53,7 +50,12 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { fetchAvailableVerbs(verbsLink); if (role) { - this.setState({ role: { ...role } }); + this.setState({ + role: { + ...role, + role: { verbs: role.verbs } + } + }); } } @@ -80,26 +82,44 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { }; handleVerbChange = (value: boolean, name: string) => { - const { selectedVerbs } = this.props; - const newVerbs = { ...selectedVerbs, [name]: value }; - this.setState({ verbs: newVerbs }); + const { role } = this.state; + + const newVerbs = value + ? [...role.verbs, name] + : role.verbs.filter(v => v !== name); + + this.setState({ + ...this.state, + role: { + ...role, + verbs: newVerbs + } + }); + }; + + submit = (event: Event) => { + event.preventDefault(); + if (this.isValid()) { + // this.props.submitForm(this.state.role); + //TODO ADD createRole here + } }; render() { - const { loading, availableVerbs, t } = this.props; - const { role, verbs } = this.state; + const { loading, availableVerbs, disabled, t } = this.props; + const { role } = this.state; - const verbSelectBoxes = - !!availableVerbs && - Object.entries(availableVerbs).map(e => ( - <PermissionCheckbox - key={e[0]} - // disabled={readOnly} - name={e[0]} - checked={e[1]} - onChange={this.handleVerbChange} - /> - )); + const verbSelectBoxes = !availableVerbs + ? null + : availableVerbs.map(verb => ( + <PermissionCheckbox + key={verb} + // disabled={readOnly} + name={verb} + checked={role.verbs.includes(verb)} + onChange={this.handleVerbChange} + /> + )); return ( <form onSubmit={this.submit}> @@ -110,17 +130,18 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { label={t("roles.create.name")} onChange={this.handleNameChange} value={role.name ? role.name : ""} - disabled={!!role.name} // || disabled + disabled={!!role.name || disabled} /> </div> </div> <>{verbSelectBoxes}</> + <hr /> <div className="columns"> <div className="column"> <SubmitButton loading={loading} label={t("roles.create.submit")} - //disabled={disabled || !this.isValid()} + disabled={disabled || !this.isValid()} /> </div> </div> diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index 7088d46fb8..8d9c99f0bc 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -220,12 +220,7 @@ function verbReducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_VERBS_SUCCESS: const verbs = action.payload.verbs; - const verbMap = {}; - verbs.forEach(p => (verbMap[p] = false)); - return { - ...state, - verbMap - }; + return { ...state, verbs }; default: return state; } From dfbc2e2cfd111a5bb5549e38c16aabe5c00632a5 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 09:50:20 +0200 Subject: [PATCH 57/91] added create, modify, delete role actions in module --- scm-ui/src/config/modules/roles.js | 227 ++++++++++++++++++++---- scm-ui/src/config/modules/roles.test.js | 186 ++++++++++++++++++- 2 files changed, 372 insertions(+), 41 deletions(-) diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/modules/roles.js index 8d9c99f0bc..9050fb54df 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/modules/roles.js @@ -16,17 +16,28 @@ export const FETCH_ROLE_PENDING = `${FETCH_ROLE}_${types.PENDING_SUFFIX}`; export const FETCH_ROLE_SUCCESS = `${FETCH_ROLE}_${types.SUCCESS_SUFFIX}`; export const FETCH_ROLE_FAILURE = `${FETCH_ROLE}_${types.FAILURE_SUFFIX}`; -export const FETCH_VERBS = "scm/roles/FETCH_VERBS"; -export const FETCH_VERBS_PENDING = `${FETCH_VERBS}_${types.PENDING_SUFFIX}`; -export const FETCH_VERBS_SUCCESS = `${FETCH_VERBS}_${types.SUCCESS_SUFFIX}`; -export const FETCH_VERBS_FAILURE = `${FETCH_VERBS}_${types.FAILURE_SUFFIX}`; - export const CREATE_ROLE = "scm/roles/CREATE_ROLE"; export const CREATE_ROLE_PENDING = `${CREATE_ROLE}_${types.PENDING_SUFFIX}`; export const CREATE_ROLE_SUCCESS = `${CREATE_ROLE}_${types.SUCCESS_SUFFIX}`; export const CREATE_ROLE_FAILURE = `${CREATE_ROLE}_${types.FAILURE_SUFFIX}`; export const CREATE_ROLE_RESET = `${CREATE_ROLE}_${types.RESET_SUFFIX}`; +export const MODIFY_ROLE = "scm/roles/MODIFY_ROLE"; +export const MODIFY_ROLE_PENDING = `${MODIFY_ROLE}_${types.PENDING_SUFFIX}`; +export const MODIFY_ROLE_SUCCESS = `${MODIFY_ROLE}_${types.SUCCESS_SUFFIX}`; +export const MODIFY_ROLE_FAILURE = `${MODIFY_ROLE}_${types.FAILURE_SUFFIX}`; +export const MODIFY_ROLE_RESET = `${MODIFY_ROLE}_${types.RESET_SUFFIX}`; + +export const DELETE_ROLE = "scm/roles/DELETE_ROLE"; +export const DELETE_ROLE_PENDING = `${DELETE_ROLE}_${types.PENDING_SUFFIX}`; +export const DELETE_ROLE_SUCCESS = `${DELETE_ROLE}_${types.SUCCESS_SUFFIX}`; +export const DELETE_ROLE_FAILURE = `${DELETE_ROLE}_${types.FAILURE_SUFFIX}`; + +export const FETCH_VERBS = "scm/roles/FETCH_VERBS"; +export const FETCH_VERBS_PENDING = `${FETCH_VERBS}_${types.PENDING_SUFFIX}`; +export const FETCH_VERBS_SUCCESS = `${FETCH_VERBS}_${types.SUCCESS_SUFFIX}`; +export const FETCH_VERBS_FAILURE = `${FETCH_VERBS}_${types.FAILURE_SUFFIX}`; + const CONTENT_TYPE_ROLE = "application/vnd.scmm-repositoryRole+json;v=2"; // fetch roles @@ -72,13 +83,8 @@ export function fetchRoles(link: string) { return fetchRolesByLink(link); } -export function fetchRolesByPage(link: string, page: number, filter?: string) { +export function fetchRolesByPage(link: string, page: number) { // backend start counting by 0 - if (filter) { - return fetchRolesByLink( - `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` - ); - } return fetchRolesByLink(`${link}?page=${page - 1}`); } @@ -226,24 +232,104 @@ function verbReducer(state: any = {}, action: any = {}) { } } -function listReducer(state: any = {}, action: any = {}) { - switch (action.type) { - case FETCH_ROLES_SUCCESS: - const roles = action.payload._embedded.repositoryRoles; - const roleNames = roles.map(role => role.name); - return { - ...state, - entries: roleNames, - entry: { - roleCreatePermission: !!action.payload._links.create, - page: action.payload.page, - pageTotal: action.payload.pageTotal, - _links: action.payload._links +// modify role +export function modifyRolePending(role: Role): Action { + return { + type: MODIFY_ROLE_PENDING, + payload: role, + itemId: role.name + }; +} + +export function modifyRoleSuccess(role: Role): Action { + return { + type: MODIFY_ROLE_SUCCESS, + payload: role, + itemId: role.name + }; +} + +export function modifyRoleFailure(role: Role, error: Error): Action { + return { + type: MODIFY_ROLE_FAILURE, + payload: { + error, + role + }, + itemId: role.name + }; +} + +export function modifyRoleReset(role: Role): Action { + return { + type: MODIFY_ROLE_RESET, + itemId: role.name + }; +} + +export function modifyRole(role: Role, callback?: () => void) { + return function(dispatch: Dispatch) { + dispatch(modifyRolePending(role)); + return apiClient + .put(role._links.update.href, role, CONTENT_TYPE_ROLE) + .then(() => { + dispatch(modifyRoleSuccess(role)); + if (callback) { + callback(); } - }; - default: - return state; - } + }) + .then(() => { + dispatch(fetchRoleByLink(role)); + }) + .catch(err => { + dispatch(modifyRoleFailure(role, err)); + }); + }; +} + +// delete role +export function deleteRolePending(role: Role): Action { + return { + type: DELETE_ROLE_PENDING, + payload: role, + itemId: role.name + }; +} + +export function deleteRoleSuccess(role: Role): Action { + return { + type: DELETE_ROLE_SUCCESS, + payload: role, + itemId: role.name + }; +} + +export function deleteRoleFailure(role: Role, error: Error): Action { + return { + type: DELETE_ROLE_FAILURE, + payload: { + error, + role + }, + itemId: role.name + }; +} + +export function deleteRole(role: Role, callback?: () => void) { + return function(dispatch: any) { + dispatch(deleteRolePending(role)); + return apiClient + .delete(role._links.delete.href) + .then(() => { + dispatch(deleteRoleSuccess(role)); + if (callback) { + callback(); + } + }) + .catch(error => { + dispatch(deleteRoleFailure(role, error)); + }); + }; } function extractRolesByNames( @@ -263,6 +349,22 @@ function extractRolesByNames( return rolesByNames; } +function deleteRoleInRolesByNames(roles: {}, roleName: string) { + let newRoles = {}; + for (let rolename in roles) { + if (rolename !== roleName) newRoles[rolename] = roles[rolename]; + } + return newRoles; +} + +function deleteRoleInEntries(roles: [], roleName: string) { + let newRoles = []; + for (let role of roles) { + if (role !== roleName) newRoles.push(role); + } + return newRoles; +} + const reducerByName = (state: any, rolename: string, newRoleState: any) => { return { ...state, @@ -270,6 +372,37 @@ const reducerByName = (state: any, rolename: string, newRoleState: any) => { }; }; +function listReducer(state: any = {}, action: any = {}) { + switch (action.type) { + case FETCH_ROLES_SUCCESS: + const roles = action.payload._embedded.repositoryRoles; + const roleNames = roles.map(role => role.name); + return { + ...state, + entries: roleNames, + entry: { + roleCreatePermission: !!action.payload._links.create, + page: action.payload.page, + pageTotal: action.payload.pageTotal, + _links: action.payload._links + } + }; + + // Delete single role actions + case DELETE_ROLE_SUCCESS: + const newRoleEntries = deleteRoleInEntries( + state.entries, + action.payload.name + ); + return { + ...state, + entries: newRoleEntries + }; + default: + return state; + } +} + function byNamesReducer(state: any = {}, action: any = {}) { switch (action.type) { // Fetch all roles actions @@ -280,9 +413,14 @@ function byNamesReducer(state: any = {}, action: any = {}) { return { ...byNames }; + // Fetch single role actions case FETCH_ROLE_SUCCESS: return reducerByName(state, action.payload.name, action.payload); + + case DELETE_ROLE_SUCCESS: + return deleteRoleInRolesByNames(state, action.payload.name); + default: return state; } @@ -301,7 +439,6 @@ const selectList = (state: Object) => { } return {}; }; - const selectListEntry = (state: Object): Object => { const list = selectList(state); if (list.entry) { @@ -344,14 +481,6 @@ export function getFetchRolesFailure(state: Object) { return getFailure(state, FETCH_ROLES); } -export function isCreateRolePending(state: Object) { - return isPending(state, CREATE_ROLE); -} - -export function getCreateRoleFailure(state: Object) { - return getFailure(state, CREATE_ROLE); -} - export function isFetchVerbsPending(state: Object) { return isPending(state, FETCH_VERBS); } @@ -360,6 +489,14 @@ export function getFetchVerbsFailure(state: Object) { return getFailure(state, FETCH_VERBS); } +export function isCreateRolePending(state: Object) { + return isPending(state, CREATE_ROLE); +} + +export function getCreateRoleFailure(state: Object) { + return getFailure(state, CREATE_ROLE); +} + export function getRoleByName(state: Object, name: string) { if (state.roles && state.roles.byNames) { return state.roles.byNames[name]; @@ -373,3 +510,19 @@ export function isFetchRolePending(state: Object, name: string) { export function getFetchRoleFailure(state: Object, name: string) { return getFailure(state, FETCH_ROLE, name); } + +export function isModifyRolePending(state: Object, name: string) { + return isPending(state, MODIFY_ROLE, name); +} + +export function getModifyRoleFailure(state: Object, name: string) { + return getFailure(state, MODIFY_ROLE, name); +} + +export function isDeleteRolePending(state: Object, name: string) { + return isPending(state, DELETE_ROLE, name); +} + +export function getDeleteRoleFailure(state: Object, name: string) { + return getFailure(state, DELETE_ROLE, name); +} diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/modules/roles.test.js index fc165f863a..3c09bb7db1 100644 --- a/scm-ui/src/config/modules/roles.test.js +++ b/scm-ui/src/config/modules/roles.test.js @@ -16,6 +16,14 @@ import reducer, { CREATE_ROLE_PENDING, CREATE_ROLE_SUCCESS, CREATE_ROLE_FAILURE, + MODIFY_ROLE, + MODIFY_ROLE_PENDING, + MODIFY_ROLE_SUCCESS, + MODIFY_ROLE_FAILURE, + DELETE_ROLE, + DELETE_ROLE_PENDING, + DELETE_ROLE_SUCCESS, + DELETE_ROLE_FAILURE, fetchRoles, getFetchRolesFailure, getRolesFromState, @@ -30,6 +38,13 @@ import reducer, { isCreateRolePending, getCreateRoleFailure, getRoleByName, + modifyRole, + isModifyRolePending, + getModifyRoleFailure, + deleteRole, + isDeleteRolePending, + deleteRoleSuccess, + getDeleteRoleFailure, selectListAsCollection, isPermittedToCreateRoles } from "./roles"; @@ -101,7 +116,8 @@ const response = { const URL = "repositoryRoles"; const ROLES_URL = "/api/v2/repositoryRoles"; -const ROLE1_URL = "http://localhost:8081/scm/api/v2/repositoryRoles/specialrole"; +const ROLE1_URL = + "http://localhost:8081/scm/api/v2/repositoryRoles/specialrole"; const error = new Error("FEHLER!"); @@ -247,6 +263,99 @@ describe("repository roles fetch", () => { expect(callMe).toBe("yeah"); }); }); + + it("successfully update role", () => { + fetchMock.putOnce(ROLE1_URL, { + status: 204 + }); + fetchMock.getOnce(ROLE1_URL, role1); + + const store = mockStore({}); + return store.dispatch(modifyRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions.length).toBe(3); + expect(actions[0].type).toEqual(MODIFY_ROLE_PENDING); + expect(actions[1].type).toEqual(MODIFY_ROLE_SUCCESS); + expect(actions[2].type).toEqual(FETCH_ROLE_PENDING); + }); + }); + + it("should call callback, after successful modified role", () => { + fetchMock.putOnce(ROLE1_URL, { + status: 204 + }); + fetchMock.getOnce(ROLE1_URL, role1); + + let called = false; + const callMe = () => { + called = true; + }; + + const store = mockStore({}); + return store.dispatch(modifyRole(role1, callMe)).then(() => { + expect(called).toBeTruthy(); + }); + }); + + it("should fail updating role on HTTP 500", () => { + fetchMock.putOnce(ROLE1_URL, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(modifyRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_ROLE_PENDING); + expect(actions[1].type).toEqual(MODIFY_ROLE_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should delete successfully role1", () => { + fetchMock.deleteOnce(ROLE1_URL, { + status: 204 + }); + + const store = mockStore({}); + return store.dispatch(deleteRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions.length).toBe(2); + expect(actions[0].type).toEqual(DELETE_ROLE_PENDING); + expect(actions[0].payload).toBe(role1); + expect(actions[1].type).toEqual(DELETE_ROLE_SUCCESS); + }); + }); + + it("should call the callback after successful delete", () => { + fetchMock.deleteOnce(ROLE1_URL, { + status: 204 + }); + + let called = false; + const callMe = () => { + called = true; + }; + + const store = mockStore({}); + return store.dispatch(deleteRole(role1, callMe)).then(() => { + expect(called).toBeTruthy(); + }); + }); + + it("should fail to delete role1", () => { + fetchMock.deleteOnce(ROLE1_URL, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(deleteRole(role1)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(DELETE_ROLE_PENDING); + expect(actions[0].payload).toBe(role1); + expect(actions[1].type).toEqual(DELETE_ROLE_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); }); describe("repository roles reducer", () => { @@ -289,6 +398,23 @@ describe("repository roles reducer", () => { expect(newState.byNames["WRITE"]).toBeDefined(); }); + it("should remove role from state when delete succeeds", () => { + const state = { + list: { + entries: ["WRITE", "specialrole"] + }, + byNames: { + specialrole: role1, + WRITE: role2 + } + }; + + const newState = reducer(state, deleteRoleSuccess(role2)); + expect(newState.byNames["specialrole"]).toBeDefined(); + expect(newState.byNames["WRITE"]).toBeFalsy(); + expect(newState.list.entries).toEqual(["specialrole"]); + }); + it("should set roleCreatePermission to true if create link is present", () => { const newState = reducer({}, fetchRolesSuccess(responseBody)); @@ -341,9 +467,9 @@ describe("repository roles selector", () => { it("should return false", () => { expect(isPermittedToCreateRoles({})).toBe(false); - expect( - isPermittedToCreateRoles({ roles: { list: { entry: {} } } }) - ).toBe(false); + expect(isPermittedToCreateRoles({ roles: { list: { entry: {} } } })).toBe( + false + ); expect( isPermittedToCreateRoles({ roles: { list: { entry: { roleCreatePermission: false } } } @@ -472,4 +598,56 @@ describe("repository roles selector", () => { it("should return undefined when fetch role2 did not fail", () => { expect(getFetchRoleFailure({}, "role2")).toBe(undefined); }); + + it("should return true, when modify role1 is pending", () => { + const state = { + pending: { + [MODIFY_ROLE + "/role1"]: true + } + }; + expect(isModifyRolePending(state, "role1")).toEqual(true); + }); + + it("should return false, when modify role1 is not pending", () => { + expect(isModifyRolePending({}, "role1")).toEqual(false); + }); + + it("should return error when modify role1 did fail", () => { + const state = { + failure: { + [MODIFY_ROLE + "/role1"]: error + } + }; + expect(getModifyRoleFailure(state, "role1")).toEqual(error); + }); + + it("should return undefined when modify role1 did not fail", () => { + expect(getModifyRoleFailure({}, "role1")).toBe(undefined); + }); + + it("should return true, when delete role2 is pending", () => { + const state = { + pending: { + [DELETE_ROLE + "/role2"]: true + } + }; + expect(isDeleteRolePending(state, "role2")).toEqual(true); + }); + + it("should return false, when delete role2 is not pending", () => { + expect(isDeleteRolePending({}, "role2")).toEqual(false); + }); + + it("should return error when delete role2 did fail", () => { + const state = { + failure: { + [DELETE_ROLE + "/role2"]: error + } + }; + expect(getDeleteRoleFailure(state, "role2")).toEqual(error); + }); + + it("should return undefined when delete role2 did not fail", () => { + expect(getDeleteRoleFailure({}, "role2")).toBe(undefined); + }); }); From 1d4adf0978a8c97bbb72135417da1a8a7a2afbcd Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 10:42:18 +0200 Subject: [PATCH 58/91] fix translation --- scm-ui/public/locales/de/config.json | 6 +++--- scm-ui/public/locales/en/config.json | 4 ++-- scm-ui/src/config/containers/GlobalPermissionRoleForm.js | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index b3d845acc8..cfa610042f 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -10,12 +10,12 @@ "navLink": "Berechtigungsrollen", "title": "Berechtigungsrollen", "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", + "system": "System", "createButton": "Berechtigungsrolle erstellen", "form": { - "name": "Name", - "system": "System", - "permissions": "Berechtigungen", "subtitle": "Berechtigungsrolle bearbeiten", + "name": "Name", + "permissions": "Berechtigungen", "submit": "Speichern" } }, diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 404dfb7e14..9d4fa65efb 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -10,12 +10,12 @@ "navLink": "Permission Roles", "title": "Permission Roles", "noPermissionRoles": "No permission roles found.", + "system": "System", "createButton": "Create Permission Role", "form": { + "subtitle": "Edit Permission Role", "name": "Name", - "system": "System", "permissions": "Permissions", - "subtitle": "Edit Permission Roles", "submit": "Save" } }, diff --git a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js index 77e743d22b..781268412d 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js +++ b/scm-ui/src/config/containers/GlobalPermissionRoleForm.js @@ -127,20 +127,21 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { <div className="column"> <InputField name="name" - label={t("roles.create.name")} + label={t("roles.form.name")} onChange={this.handleNameChange} value={role.name ? role.name : ""} disabled={!!role.name || disabled} /> </div> </div> - <>{verbSelectBoxes}</> + + {verbSelectBoxes} <hr /> <div className="columns"> <div className="column"> <SubmitButton loading={loading} - label={t("roles.create.submit")} + label={t("roles.form.submit")} disabled={disabled || !this.isValid()} /> </div> @@ -175,4 +176,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("roles")(GlobalPermissionRoleForm)); +)(translate("config")(GlobalPermissionRoleForm)); From 8b4e6ab72d93d1b9f9f1d4da3b931aa7662ef8ca Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 10:43:13 +0200 Subject: [PATCH 59/91] add systemRoleTag for systemRoles --- scm-ui/src/config/components/SystemRoleTag.js | 35 +++++++++++++++++++ .../components/table/PermissionRoleRow.js | 14 ++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 scm-ui/src/config/components/SystemRoleTag.js diff --git a/scm-ui/src/config/components/SystemRoleTag.js b/scm-ui/src/config/components/SystemRoleTag.js new file mode 100644 index 0000000000..735d4a84af --- /dev/null +++ b/scm-ui/src/config/components/SystemRoleTag.js @@ -0,0 +1,35 @@ +//@flow +import React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { translate } from "react-i18next"; + +type Props = { + system?: boolean, + classes: any, + t: string => string +}; + +const styles = { + tag: { + marginLeft: "0.75rem", + verticalAlign: "inherit" + } +}; + +class SystemRoleTag extends React.Component<Props> { + render() { + const { system, classes, t } = this.props; + + if (system) { + return ( + <span className={classNames("tag is-dark", classes.tag)}> + {t("roles.system")} + </span> + ); + } + return null; + } +} + +export default injectSheet(styles)(translate("config")(SystemRoleTag)); diff --git a/scm-ui/src/config/components/table/PermissionRoleRow.js b/scm-ui/src/config/components/table/PermissionRoleRow.js index f42627540c..f556387abf 100644 --- a/scm-ui/src/config/components/table/PermissionRoleRow.js +++ b/scm-ui/src/config/components/table/PermissionRoleRow.js @@ -2,6 +2,7 @@ import React from "react"; import { Link } from "react-router-dom"; import type { Role } from "@scm-manager/ui-types"; +import SystemRoleTag from "../SystemRoleTag"; type Props = { baseUrl: string, @@ -9,8 +10,15 @@ type Props = { }; class PermissionRoleRow extends React.Component<Props> { - renderLink(to: string, label: string) { - return <Link to={to}>{label}</Link>; + renderLink(to: string, label: string, system?: boolean) { + if (!system) { + return <Link to={to}>{label}</Link>; + } + return ( + <> + {label} <SystemRoleTag system={system} /> + </> + ); } render() { @@ -18,7 +26,7 @@ class PermissionRoleRow extends React.Component<Props> { const to = `${baseUrl}/${encodeURIComponent(role.name)}/edit`; return ( <tr> - <td>{this.renderLink(to, role.name)}</td> + <td>{this.renderLink(to, role.name, !role._links.update)}</td> </tr> ); } From 661f62204ae4083747400fca33f9f3192a7c901f Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 12:19:47 +0200 Subject: [PATCH 60/91] reorder directory structure, add PermissionRoleDetail and Root --- scm-ui/public/locales/en/config.json | 5 +- scm-ui/src/config/containers/Config.js | 25 +++-- .../roles/components/PermissionRoleDetail.js | 29 +++++ .../components}/PermissionRoleRow.js | 11 +- .../components}/PermissionRoleTable.js | 0 .../{ => roles}/components/SystemRoleTag.js | 2 +- .../roles/containers/CreatePermissionRole.js | 12 ++ .../containers/GlobalPermissionRoleForm.js | 4 +- .../roles/containers/PermissionRoleRoot.js | 103 ++++++++++++++++++ .../containers/PermissionRolesOverview.js} | 9 +- .../src/config/{ => roles}/modules/roles.js | 19 +++- .../config/{ => roles}/modules/roles.test.js | 0 scm-ui/src/createReduxStore.js | 2 +- 13 files changed, 191 insertions(+), 30 deletions(-) create mode 100644 scm-ui/src/config/roles/components/PermissionRoleDetail.js rename scm-ui/src/config/{components/table => roles/components}/PermissionRoleRow.js (74%) rename scm-ui/src/config/{components/table => roles/components}/PermissionRoleTable.js (100%) rename scm-ui/src/config/{ => roles}/components/SystemRoleTag.js (95%) create mode 100644 scm-ui/src/config/roles/containers/CreatePermissionRole.js rename scm-ui/src/config/{ => roles}/containers/GlobalPermissionRoleForm.js (95%) create mode 100644 scm-ui/src/config/roles/containers/PermissionRoleRoot.js rename scm-ui/src/config/{containers/GlobalPermissionRoles.js => roles/containers/PermissionRolesOverview.js} (91%) rename scm-ui/src/config/{ => roles}/modules/roles.js (96%) rename scm-ui/src/config/{ => roles}/modules/roles.test.js (100%) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 9d4fa65efb..e4ecc39874 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -10,7 +10,6 @@ "navLink": "Permission Roles", "title": "Permission Roles", "noPermissionRoles": "No permission roles found.", - "system": "System", "createButton": "Create Permission Role", "form": { "subtitle": "Edit Permission Role", @@ -19,6 +18,10 @@ "submit": "Save" } }, + "role": { + "name": "Name", + "system": "System" + }, "config-form": { "submit": "Submit", "submit-success-notification": "Configuration changed successfully!", diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index ee973cf64b..c405d7b164 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -10,8 +10,9 @@ import type { Links } from "@scm-manager/ui-types"; import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; import { getLinks } from "../../modules/indexResource"; import GlobalConfig from "./GlobalConfig"; -import GlobalPermissionRoles from "./GlobalPermissionRoles"; -import GlobalPermissionRoleForm from "./GlobalPermissionRoleForm"; +import PermissionRolesOverview from "../roles/containers/PermissionRolesOverview"; +import PermissionRoleRoot from "../roles/containers/PermissionRoleRoot"; +import CreatePermissionRole from "../roles/containers/CreatePermissionRole"; type Props = { links: Links, @@ -36,7 +37,7 @@ class Config extends React.Component<Props> { matchesRoles = (route: any) => { const url = this.matchedUrl(); - const regex = new RegExp(`${url}/role/.+/edit`); + const regex = new RegExp(`${url}/role/.+/info`); return route.location.pathname.match(regex); }; @@ -54,16 +55,19 @@ class Config extends React.Component<Props> { <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact component={GlobalConfig} /> + <Route + path={`${url}/role/:role`} + component={() => <PermissionRoleRoot baseUrl={`${url}/roles`} />} + /> <Route path={`${url}/roles`} exact - render={() => ( - <GlobalPermissionRoles - baseUrl={`${url}/role`} - /> - )} + render={() => <PermissionRolesOverview baseUrl={`${url}/role`} />} + /> + <Route + path={`${url}/roles/create`} + render={() => <CreatePermissionRole />} /> - <Route path={`${url}/role`} component={GlobalPermissionRoleForm} /> <ExtensionPoint name="config.route" props={extensionProps} @@ -78,9 +82,10 @@ class Config extends React.Component<Props> { label={t("config.globalConfigurationNavLink")} /> <NavLink - to={`${url}/roles`} + to={`${url}/roles/`} label={t("roles.navLink")} activeWhenMatch={this.matchesRoles} + activeOnlyWhenExact={false} /> <ExtensionPoint name="config.navigation" diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetail.js b/scm-ui/src/config/roles/components/PermissionRoleDetail.js new file mode 100644 index 0000000000..496443eef2 --- /dev/null +++ b/scm-ui/src/config/roles/components/PermissionRoleDetail.js @@ -0,0 +1,29 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Role } from "@scm-manager/ui-types"; +import SystemRoleTag from "./SystemRoleTag"; + +type Props = { + role: Role, + + // context props + t: string => string, +}; + +class PermissionRoleDetail extends React.Component<Props> { + render() { + const { role, t } = this.props; + + return ( + <div className="media"> + <div className="media-content subtitle"> + <strong>{t("role.name")}:</strong> {role.name}{" "} + <SystemRoleTag system={!role._links.update} /> + </div> + </div> + ); + } +} + +export default translate("config")(PermissionRoleDetail); diff --git a/scm-ui/src/config/components/table/PermissionRoleRow.js b/scm-ui/src/config/roles/components/PermissionRoleRow.js similarity index 74% rename from scm-ui/src/config/components/table/PermissionRoleRow.js rename to scm-ui/src/config/roles/components/PermissionRoleRow.js index f556387abf..ba38fc5c4e 100644 --- a/scm-ui/src/config/components/table/PermissionRoleRow.js +++ b/scm-ui/src/config/roles/components/PermissionRoleRow.js @@ -2,7 +2,7 @@ import React from "react"; import { Link } from "react-router-dom"; import type { Role } from "@scm-manager/ui-types"; -import SystemRoleTag from "../SystemRoleTag"; +import SystemRoleTag from "./SystemRoleTag"; type Props = { baseUrl: string, @@ -11,19 +11,16 @@ type Props = { class PermissionRoleRow extends React.Component<Props> { renderLink(to: string, label: string, system?: boolean) { - if (!system) { - return <Link to={to}>{label}</Link>; - } return ( - <> + <Link to={to}> {label} <SystemRoleTag system={system} /> - </> + </Link> ); } render() { const { baseUrl, role } = this.props; - const to = `${baseUrl}/${encodeURIComponent(role.name)}/edit`; + const to = `${baseUrl}/${encodeURIComponent(role.name)}/info`; return ( <tr> <td>{this.renderLink(to, role.name, !role._links.update)}</td> diff --git a/scm-ui/src/config/components/table/PermissionRoleTable.js b/scm-ui/src/config/roles/components/PermissionRoleTable.js similarity index 100% rename from scm-ui/src/config/components/table/PermissionRoleTable.js rename to scm-ui/src/config/roles/components/PermissionRoleTable.js diff --git a/scm-ui/src/config/components/SystemRoleTag.js b/scm-ui/src/config/roles/components/SystemRoleTag.js similarity index 95% rename from scm-ui/src/config/components/SystemRoleTag.js rename to scm-ui/src/config/roles/components/SystemRoleTag.js index 735d4a84af..0f6b87addc 100644 --- a/scm-ui/src/config/components/SystemRoleTag.js +++ b/scm-ui/src/config/roles/components/SystemRoleTag.js @@ -24,7 +24,7 @@ class SystemRoleTag extends React.Component<Props> { if (system) { return ( <span className={classNames("tag is-dark", classes.tag)}> - {t("roles.system")} + {t("role.system")} </span> ); } diff --git a/scm-ui/src/config/roles/containers/CreatePermissionRole.js b/scm-ui/src/config/roles/containers/CreatePermissionRole.js new file mode 100644 index 0000000000..6888d7493d --- /dev/null +++ b/scm-ui/src/config/roles/containers/CreatePermissionRole.js @@ -0,0 +1,12 @@ +// @flow +import React from "react"; + +type Props = {}; + +class CreatePermissionRole extends React.Component<Props> { + render() { + return <>yep</>; + } +} + +export default CreatePermissionRole; diff --git a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js b/scm-ui/src/config/roles/containers/GlobalPermissionRoleForm.js similarity index 95% rename from scm-ui/src/config/containers/GlobalPermissionRoleForm.js rename to scm-ui/src/config/roles/containers/GlobalPermissionRoleForm.js index 781268412d..10535a7196 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js +++ b/scm-ui/src/config/roles/containers/GlobalPermissionRoleForm.js @@ -4,14 +4,14 @@ import { connect } from "react-redux"; import { translate } from "react-i18next"; import type { Role } from "@scm-manager/ui-types"; import { InputField, SubmitButton } from "@scm-manager/ui-components"; -import PermissionCheckbox from "../../repos/permissions/components/PermissionCheckbox"; +import PermissionCheckbox from "../../../repos/permissions/components/PermissionCheckbox"; import { fetchAvailableVerbs, getFetchVerbsFailure, getVerbsFromState, isFetchVerbsPending } from "../modules/roles"; -import { getRepositoryVerbsLink } from "../../modules/indexResource"; +import { getRepositoryVerbsLink } from "../../../modules/indexResource"; type Props = { role?: Role, diff --git a/scm-ui/src/config/roles/containers/PermissionRoleRoot.js b/scm-ui/src/config/roles/containers/PermissionRoleRoot.js new file mode 100644 index 0000000000..d6d29f4f2e --- /dev/null +++ b/scm-ui/src/config/roles/containers/PermissionRoleRoot.js @@ -0,0 +1,103 @@ +//@flow +import React from "react"; +import { connect } from "react-redux"; +import { Redirect, Route, Switch, withRouter } from "react-router-dom"; +import type {Role} from "@scm-manager/ui-types"; +import {ErrorNotification, Loading} from "@scm-manager/ui-components"; +import { getRolesLink } from "../../../modules/indexResource"; +import { + fetchRoleByName, + getRoleByName, + isFetchRolePending, + getFetchRoleFailure +} from "../modules/roles"; +import PermissionRoleDetail from "../components/PermissionRoleDetail"; + +type Props = { + roleLink: string, + roleName: string, + role: Role, + loading: boolean, + error: Error, + + // context props + match: any, + t: string => string, + + // dispatch functions + fetchRoleByName: (roleLink: string, roleName: string) => void +}; + +class PermissionRoleRoot extends React.Component<Props> { + componentDidMount() { + const { fetchRoleByName, roleLink, roleName } = this.props; + fetchRoleByName(roleLink, roleName); + } + + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 1); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; + + render() { + const { loading, error, role} = this.props; + + const url = this.matchedUrl(); + + if (error) { + return <ErrorNotification error={error} />; + } + + if (loading || !role) { + return <Loading />; + } + + return ( + <Switch> + <Redirect exact from={url} to={`${url}/info`} /> + <Route + path={`${url}/info`} + component={() => ( + <PermissionRoleDetail role={role} /> + )} + /> + </Switch> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const roleName = decodeURIComponent(ownProps.match.params.role); + const role = getRoleByName(state, roleName); + const loading = isFetchRolePending(state, roleName); + const error = getFetchRoleFailure(state, roleName); + const roleLink = getRolesLink(state); + return { + roleName, + role, + loading, + error, + roleLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchRoleByName: (roleLink: string, roleName: string) => { + dispatch(fetchRoleByName(roleLink, roleName)); + } + }; +}; + +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(PermissionRoleRoot) +); diff --git a/scm-ui/src/config/containers/GlobalPermissionRoles.js b/scm-ui/src/config/roles/containers/PermissionRolesOverview.js similarity index 91% rename from scm-ui/src/config/containers/GlobalPermissionRoles.js rename to scm-ui/src/config/roles/containers/PermissionRolesOverview.js index 2691bf3988..2ef95ce622 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoles.js +++ b/scm-ui/src/config/roles/containers/PermissionRolesOverview.js @@ -21,8 +21,9 @@ import { isFetchRolesPending, getFetchRolesFailure } from "../modules/roles"; -import PermissionRoleTable from "../components/table/PermissionRoleTable"; -import { getRolesLink } from "../../modules/indexResource"; +import PermissionRoleTable from "../components/PermissionRoleTable"; +import { getRolesLink } from "../../../modules/indexResource"; + type Props = { baseUrl: string, roles: Role[], @@ -41,7 +42,7 @@ type Props = { fetchRolesByPage: (link: string, page: number) => void }; -class GlobalPermissionRoles extends React.Component<Props> { +class PermissionRolesOverview extends React.Component<Props> { componentDidMount() { const { fetchRolesByPage, rolesLink, page } = this.props; fetchRolesByPage(rolesLink, page); @@ -119,4 +120,4 @@ const mapDispatchToProps = dispatch => { export default withRouter(connect( mapStateToProps, mapDispatchToProps -)(translate("config")(GlobalPermissionRoles))); +)(translate("config")(PermissionRolesOverview))); diff --git a/scm-ui/src/config/modules/roles.js b/scm-ui/src/config/roles/modules/roles.js similarity index 96% rename from scm-ui/src/config/modules/roles.js rename to scm-ui/src/config/roles/modules/roles.js index 9050fb54df..a7a3ce3d93 100644 --- a/scm-ui/src/config/modules/roles.js +++ b/scm-ui/src/config/roles/modules/roles.js @@ -1,10 +1,10 @@ // @flow import { apiClient } from "@scm-manager/ui-components"; -import { isPending } from "../../modules/pending"; -import { getFailure } from "../../modules/failure"; -import * as types from "../../modules/types"; +import { isPending } from "../../../modules/pending"; +import { getFailure } from "../../../modules/failure"; +import * as types from "../../../modules/types"; import { combineReducers, Dispatch } from "redux"; -import type { Action, PagedCollection, Role } from "@scm-manager/ui-types"; +import type {Action, PagedCollection, Repository, Role} from "@scm-manager/ui-types"; export const FETCH_ROLES = "scm/roles/FETCH_ROLES"; export const FETCH_ROLES_PENDING = `${FETCH_ROLES}_${types.PENDING_SUFFIX}`; @@ -469,6 +469,17 @@ export function getRolesFromState(state: Object) { return roleEntries; } +export function getRoleCreateLink(state: Object) { + if ( + state && + state.list && + state.list._links && + state.list._links.create + ) { + return state.list._links.create.href; + } +} + export function getVerbsFromState(state: Object) { return state.roles.verbs.verbs; } diff --git a/scm-ui/src/config/modules/roles.test.js b/scm-ui/src/config/roles/modules/roles.test.js similarity index 100% rename from scm-ui/src/config/modules/roles.test.js rename to scm-ui/src/config/roles/modules/roles.test.js diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 70ad9abd5b..500ea23752 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -15,7 +15,7 @@ import pending from "./modules/pending"; import failure from "./modules/failure"; import permissions from "./repos/permissions/modules/permissions"; import config from "./config/modules/config"; -import roles from "./config/modules/roles"; +import roles from "./config/roles/modules/roles"; import namespaceStrategies from "./config/modules/namespaceStrategies"; import indexResources from "./modules/indexResource"; From 8fd034d599a38fdde3defadd61ee181abd8015f9 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 15 May 2019 13:13:48 +0200 Subject: [PATCH 61/91] refactor / new structure for repositoryRoles --- .../navLinks/EditRepositoryRoleNavLink.js | 28 ++++ .../EditRepositoryRoleNavLink.test.js | 27 ++++ .../navLinks/RepositoryRoleDetailNavLink.js | 28 ++++ .../RepositoryRoleDetailNavLink.test.js | 31 +++++ .../src/config/components/navLinks/index.js | 2 + .../components/table/PermissionRoleRow.js | 3 +- scm-ui/src/config/containers/Config.js | 13 +- ...ssionRoleForm.js => RepositoryRoleForm.js} | 31 ++--- ...lPermissionRoles.js => RepositoryRoles.js} | 4 +- .../config/containers/SingleRepositoryRole.js | 121 ++++++++++++++++++ 10 files changed, 260 insertions(+), 28 deletions(-) create mode 100644 scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js create mode 100644 scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js create mode 100644 scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js create mode 100644 scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js create mode 100644 scm-ui/src/config/components/navLinks/index.js rename scm-ui/src/config/containers/{GlobalPermissionRoleForm.js => RepositoryRoleForm.js} (84%) rename scm-ui/src/config/containers/{GlobalPermissionRoles.js => RepositoryRoles.js} (96%) create mode 100644 scm-ui/src/config/containers/SingleRepositoryRole.js diff --git a/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js b/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js new file mode 100644 index 0000000000..419ca330cc --- /dev/null +++ b/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import type { User } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; + +type Props = { + user: User, + editUrl: String, + t: string => string +}; + +class EditRepositoryRoleNavLink extends React.Component<Props> { + isEditable = () => { + return this.props.user._links.update; + }; + + render() { + const { t, editUrl } = this.props; + + if (!this.isEditable()) { + return null; + } + return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} />; + } +} + +export default translate("users")(EditRepositoryRoleNavLink); diff --git a/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js b/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js new file mode 100644 index 0000000000..7e86e9bcda --- /dev/null +++ b/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js @@ -0,0 +1,27 @@ +import React from "react"; +import { shallow } from "enzyme"; +import "../../../tests/enzyme"; +import "../../../tests/i18n"; +import EditUserNavLink from "./EditRepositoryRoleNavLink"; + +it("should render nothing, if the edit link is missing", () => { + const user = { + _links: {} + }; + + const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>); + expect(navLink.text()).toBe(""); +}); + +it("should render the navLink", () => { + const user = { + _links: { + update: { + href: "/users" + } + } + }; + + const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>); + expect(navLink.text()).not.toBe(""); +}); diff --git a/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js b/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js new file mode 100644 index 0000000000..cc04aa6b50 --- /dev/null +++ b/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { User } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + user: User, + permissionsUrl: String +}; + +class ChangePermissionNavLink extends React.Component<Props> { + render() { + const { t, permissionsUrl } = this.props; + + // if (!this.hasPermissionToSetPermission()) { + // return null; + // } + return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} />; + } + + // hasPermissionToSetPermission = () => { + // return this.props.user._links.permissions; + // }; +} + +export default translate("users")(ChangePermissionNavLink); diff --git a/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js b/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js new file mode 100644 index 0000000000..8cf5231387 --- /dev/null +++ b/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js @@ -0,0 +1,31 @@ +import React from "react"; +import { shallow } from "enzyme"; +import "../../../tests/enzyme"; +import "../../../tests/i18n"; +import SetPermissionsNavLink from "./RepositoryRoleDetailNavLink"; + +it("should render nothing, if the permissions link is missing", () => { + const user = { + _links: {} + }; + + const navLink = shallow( + <SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" /> + ); + expect(navLink.text()).toBe(""); +}); + +it("should render the navLink", () => { + const user = { + _links: { + permissions: { + href: "/permissions" + } + } + }; + + const navLink = shallow( + <SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" /> + ); + expect(navLink.text()).not.toBe(""); +}); diff --git a/scm-ui/src/config/components/navLinks/index.js b/scm-ui/src/config/components/navLinks/index.js new file mode 100644 index 0000000000..7fafde676d --- /dev/null +++ b/scm-ui/src/config/components/navLinks/index.js @@ -0,0 +1,2 @@ +export { default as EditRepositoryRoleNavLink } from "./EditRepositoryRoleNavLink"; +export { default as RepositoryRoleDetailNavLink } from "./RepositoryRoleDetailNavLink"; diff --git a/scm-ui/src/config/components/table/PermissionRoleRow.js b/scm-ui/src/config/components/table/PermissionRoleRow.js index f42627540c..73a2cbc05c 100644 --- a/scm-ui/src/config/components/table/PermissionRoleRow.js +++ b/scm-ui/src/config/components/table/PermissionRoleRow.js @@ -15,7 +15,8 @@ class PermissionRoleRow extends React.Component<Props> { render() { const { baseUrl, role } = this.props; - const to = `${baseUrl}/${encodeURIComponent(role.name)}/edit`; + const singleRoleUrl = baseUrl.substring(0, baseUrl.length - 1); + const to = `${singleRoleUrl}/${encodeURIComponent(role.name)}`; return ( <tr> <td>{this.renderLink(to, role.name)}</td> diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index ee973cf64b..38b5401926 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -10,8 +10,9 @@ import type { Links } from "@scm-manager/ui-types"; import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; import { getLinks } from "../../modules/indexResource"; import GlobalConfig from "./GlobalConfig"; -import GlobalPermissionRoles from "./GlobalPermissionRoles"; -import GlobalPermissionRoleForm from "./GlobalPermissionRoleForm"; +import RepositoryRoles from "./RepositoryRoles"; +import RepositoryRoleForm from "./RepositoryRoleForm" +import SingleRepositoryRole from "./SingleRepositoryRole"; type Props = { links: Links, @@ -57,13 +58,9 @@ class Config extends React.Component<Props> { <Route path={`${url}/roles`} exact - render={() => ( - <GlobalPermissionRoles - baseUrl={`${url}/role`} - /> - )} + render={() => <RepositoryRoles baseUrl={`${url}/roles`} />} /> - <Route path={`${url}/role`} component={GlobalPermissionRoleForm} /> + <Route path={`${url}/role`} component={SingleRepositoryRole} /> <ExtensionPoint name="config.route" props={extensionProps} diff --git a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js b/scm-ui/src/config/containers/RepositoryRoleForm.js similarity index 84% rename from scm-ui/src/config/containers/GlobalPermissionRoleForm.js rename to scm-ui/src/config/containers/RepositoryRoleForm.js index 77e743d22b..069be3c5b2 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoleForm.js +++ b/scm-ui/src/config/containers/RepositoryRoleForm.js @@ -6,12 +6,13 @@ import type { Role } from "@scm-manager/ui-types"; import { InputField, SubmitButton } from "@scm-manager/ui-components"; import PermissionCheckbox from "../../repos/permissions/components/PermissionCheckbox"; import { + createRole, fetchAvailableVerbs, getFetchVerbsFailure, getVerbsFromState, isFetchVerbsPending } from "../modules/roles"; -import { getRepositoryVerbsLink } from "../../modules/indexResource"; +import {getRepositoryRolesLink, getRepositoryVerbsLink} from "../../modules/indexResource"; type Props = { role?: Role, @@ -24,14 +25,15 @@ type Props = { t: string => string, // dispatch functions - fetchAvailableVerbs: (link: string) => void + fetchAvailableVerbs: (link: string) => void, + addRole: (link: string, role: Role) => void }; type State = { role: Role }; -class GlobalPermissionRoleForm extends React.Component<Props, State> { +class RepositoryRoleForm extends React.Component<Props, State> { constructor(props: Props) { super(props); @@ -46,17 +48,8 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { } componentDidMount() { - const { fetchAvailableVerbs, verbsLink, role } = this.props; + const { fetchAvailableVerbs, verbsLink} = this.props; fetchAvailableVerbs(verbsLink); - - if (role) { - this.setState({ - role: { - ...role, - role: { verbs: role.verbs } - } - }); - } } isFalsy(value) { @@ -100,8 +93,7 @@ class GlobalPermissionRoleForm extends React.Component<Props, State> { submit = (event: Event) => { event.preventDefault(); if (this.isValid()) { - // this.props.submitForm(this.state.role); - //TODO ADD createRole here + this.props.addRole(this.props.repositoryRolesLink, this.state.role) } }; @@ -155,12 +147,14 @@ const mapStateToProps = (state, ownProps) => { const error = getFetchVerbsFailure(state); const verbsLink = getRepositoryVerbsLink(state); const availableVerbs = getVerbsFromState(state); + const repositoryRolesLink = getRepositoryRolesLink(state); return { loading, error, verbsLink, - availableVerbs + availableVerbs, + repositoryRolesLink }; }; @@ -168,6 +162,9 @@ const mapDispatchToProps = dispatch => { return { fetchAvailableVerbs: (link: string) => { dispatch(fetchAvailableVerbs(link)); + }, + addRole: (link: string, role: Role) => { + createRole(link, role) } }; }; @@ -175,4 +172,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("roles")(GlobalPermissionRoleForm)); +)(translate("roles")(RepositoryRoleForm)); diff --git a/scm-ui/src/config/containers/GlobalPermissionRoles.js b/scm-ui/src/config/containers/RepositoryRoles.js similarity index 96% rename from scm-ui/src/config/containers/GlobalPermissionRoles.js rename to scm-ui/src/config/containers/RepositoryRoles.js index 2691bf3988..41cbd11bf5 100644 --- a/scm-ui/src/config/containers/GlobalPermissionRoles.js +++ b/scm-ui/src/config/containers/RepositoryRoles.js @@ -41,7 +41,7 @@ type Props = { fetchRolesByPage: (link: string, page: number) => void }; -class GlobalPermissionRoles extends React.Component<Props> { +class RepositoryRoles extends React.Component<Props> { componentDidMount() { const { fetchRolesByPage, rolesLink, page } = this.props; fetchRolesByPage(rolesLink, page); @@ -119,4 +119,4 @@ const mapDispatchToProps = dispatch => { export default withRouter(connect( mapStateToProps, mapDispatchToProps -)(translate("config")(GlobalPermissionRoles))); +)(translate("config")(RepositoryRoles))); diff --git a/scm-ui/src/config/containers/SingleRepositoryRole.js b/scm-ui/src/config/containers/SingleRepositoryRole.js new file mode 100644 index 0000000000..dcdba90b31 --- /dev/null +++ b/scm-ui/src/config/containers/SingleRepositoryRole.js @@ -0,0 +1,121 @@ +//@flow +import React from "react"; +import { connect } from "react-redux"; +import { + Page, + Loading, + ErrorPage +} from "@scm-manager/ui-components"; +import { Route } from "react-router"; +import type { History } from "history"; +import { EditRepositoryRoleNavLink, RepositoryRoleDetailNavLink } from "./../components/navLinks"; +import { translate } from "react-i18next"; +import type { Role } from "@scm-manager/ui-types"; +import {getRepositoryRolesLink} from "../../modules/indexResource"; +import {ExtensionPoint} from "@scm-manager/ui-extensions"; +import {fetchRoleByName, getFetchRoleFailure, getRoleByName, isFetchRolePending} from "../modules/roles"; + +type Props = { + name: string, + role: Role, + loading: boolean, + error: Error, + repositoryRolesLink: string, + + // dispatcher function + fetchRoleByName: (string, string) => void, + + // context objects + t: string => string, + match: any, + history: History +}; + +class SingleRepositoryRole extends React.Component<Props> { + componentDidMount() { + this.props.fetchRoleByName(this.props.repositoryRolesLink, this.props.name); + } + + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 2); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; + + render() { + const { t, loading, error, role } = this.props; + + if (error) { + return ( + <ErrorPage + title={t("singleUser.errorTitle")} + subtitle={t("singleUser.errorSubtitle")} + error={error} + /> + ); + } + + if (!role || loading) { + return <Loading />; + } + + const url = this.matchedUrl(); + + const extensionProps = { + role, + url + }; + + return ( + <Page title={role.displayName}> + <div className="columns"> + <div className="column is-three-quarters"> + <Route path={url} exact component={() => <RepositoryRoleDetailNavLink role={role} />} /> + <Route + path={`${url}/settings/general`} + component={() => <EditRepositoryRoleNavLink role={role} />} + /> + <ExtensionPoint + name="user.route" + props={extensionProps} + renderAll={true} + /> + </div> + </div> + </Page> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const name = ownProps.match.params.name; + const role = getRoleByName(state, name); + const loading = isFetchRolePending(state, name); + const error = getFetchRoleFailure(state, name); + const repositoryRolesLink = getRepositoryRolesLink(state); + return { + repositoryRolesLink, + name, + role, + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchRoleByName: (link: string, name: string) => { + dispatch(fetchRoleByName(link, name)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("users")(SingleRepositoryRole)); From dabba5a091df12dd0e8ac6a2160453fef14d0b00 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 14:35:05 +0200 Subject: [PATCH 62/91] remove redundancy, run prettier --- scm-ui/src/repos/branches/modules/branches.js | 5 ++-- scm-ui/src/repos/modules/changesets.js | 19 ++++++++++---- scm-ui/src/repos/modules/repos.js | 4 +-- scm-ui/src/repos/modules/repositoryTypes.js | 5 +--- .../repos/permissions/modules/permissions.js | 26 +++++++++++++------ 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/scm-ui/src/repos/branches/modules/branches.js b/scm-ui/src/repos/branches/modules/branches.js index 40228eac91..7886a0fcbf 100644 --- a/scm-ui/src/repos/branches/modules/branches.js +++ b/scm-ui/src/repos/branches/modules/branches.js @@ -307,10 +307,9 @@ const reduceByBranchesSuccess = (state, payload) => { const byName = repoState.byName || {}; repoState.byName = byName; - if(response._embedded) { + if (response._embedded) { const branches = response._embedded.branches; - const names = branches.map(b => b.name); - response._embedded.branches = names; + response._embedded.branches = branches.map(b => b.name); for (let branch of branches) { byName[branch.name] = branch; } diff --git a/scm-ui/src/repos/modules/changesets.js b/scm-ui/src/repos/modules/changesets.js index 80c405f5de..3cd617ac56 100644 --- a/scm-ui/src/repos/modules/changesets.js +++ b/scm-ui/src/repos/modules/changesets.js @@ -1,10 +1,19 @@ // @flow -import {FAILURE_SUFFIX, PENDING_SUFFIX, SUCCESS_SUFFIX} from "../../modules/types"; -import {apiClient, urls} from "@scm-manager/ui-components"; -import {isPending} from "../../modules/pending"; -import {getFailure} from "../../modules/failure"; -import type {Action, Branch, PagedCollection, Repository} from "@scm-manager/ui-types"; +import { + FAILURE_SUFFIX, + PENDING_SUFFIX, + SUCCESS_SUFFIX +} from "../../modules/types"; +import { apiClient, urls } from "@scm-manager/ui-components"; +import { isPending } from "../../modules/pending"; +import { getFailure } from "../../modules/failure"; +import type { + Action, + Branch, + PagedCollection, + Repository +} from "@scm-manager/ui-types"; export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS"; export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`; diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index cd48f89fea..ae942e668b 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -353,15 +353,13 @@ function normalizeByNamespaceAndName( const reducerByNames = (state: Object, repository: Repository) => { const identifier = createIdentifier(repository); - const newState = { + return { ...state, byNames: { ...state.byNames, [identifier]: repository } }; - - return newState; }; export default function reducer( diff --git a/scm-ui/src/repos/modules/repositoryTypes.js b/scm-ui/src/repos/modules/repositoryTypes.js index d96d24b612..043ab04d68 100644 --- a/scm-ui/src/repos/modules/repositoryTypes.js +++ b/scm-ui/src/repos/modules/repositoryTypes.js @@ -49,10 +49,7 @@ export function shouldFetchRepositoryTypes(state: Object) { ) { return false; } - if (state.repositoryTypes && state.repositoryTypes.length > 0) { - return false; - } - return true; + return !(state.repositoryTypes && state.repositoryTypes.length > 0); } export function fetchRepositoryTypesPending(): Action { diff --git a/scm-ui/src/repos/permissions/modules/permissions.js b/scm-ui/src/repos/permissions/modules/permissions.js index 2b0263bf35..2aa9d139c5 100644 --- a/scm-ui/src/repos/permissions/modules/permissions.js +++ b/scm-ui/src/repos/permissions/modules/permissions.js @@ -77,10 +77,18 @@ const CONTENT_TYPE = "application/vnd.scmm-repositoryPermission+json"; // fetch available permissions -export function fetchAvailablePermissionsIfNeeded(repositoryRolesLink: string, repositoryVerbsLink: string) { +export function fetchAvailablePermissionsIfNeeded( + repositoryRolesLink: string, + repositoryVerbsLink: string +) { return function(dispatch: any, getState: () => Object) { if (shouldFetchAvailablePermissions(getState())) { - return fetchAvailablePermissions(dispatch, getState, repositoryRolesLink, repositoryVerbsLink); + return fetchAvailablePermissions( + dispatch, + getState, + repositoryRolesLink, + repositoryVerbsLink + ); } }; } @@ -97,7 +105,8 @@ export function fetchAvailablePermissions( .then(repositoryRoles => repositoryRoles.json()) .then(repositoryRoles => repositoryRoles._embedded.repositoryRoles) .then(repositoryRoles => { - return apiClient.get(repositoryVerbsLink) + return apiClient + .get(repositoryVerbsLink) .then(repositoryVerbs => repositoryVerbs.json()) .then(repositoryVerbs => repositoryVerbs.verbs) .then(repositoryVerbs => { @@ -577,8 +586,7 @@ export function getPermissionsOfRepo( repoName: string ) { if (state.permissions && state.permissions[namespace + "/" + repoName]) { - const permissions = state.permissions[namespace + "/" + repoName].entries; - return permissions; + return state.permissions[namespace + "/" + repoName].entries; } } @@ -739,9 +747,11 @@ export function findMatchingRoleName( if (!verbs) { return ""; } - const matchingRole = !! availableRoles && availableRoles.find(role => { - return equalVerbs(role.verbs, verbs); - }); + const matchingRole = + !!availableRoles && + availableRoles.find(role => { + return equalVerbs(role.verbs, verbs); + }); if (matchingRole) { return matchingRole.name; From 77d8a3ad21bb91224fbd025cf117fb92a8acee3e Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 15 May 2019 14:48:12 +0200 Subject: [PATCH 63/91] create Custom Repo Role --- scm-ui/src/config/containers/Config.js | 3 +- .../roles/containers/CreateRepositoryRole.js | 75 +++++++++++++++++++ .../roles/containers/RepositoryRoleForm.js | 15 ++-- .../roles/containers/SingleRepositoryRole.js | 1 + 4 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 scm-ui/src/config/roles/containers/CreateRepositoryRole.js diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 75f26003d7..8c5a3b1fcc 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -12,6 +12,7 @@ import { getLinks } from "../../modules/indexResource"; import GlobalConfig from "./GlobalConfig"; import RepositoryRoles from "../roles/containers/RepositoryRoles"; import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole"; +import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole"; type Props = { links: Links, @@ -65,7 +66,7 @@ class Config extends React.Component<Props> { /> <Route path={`${url}/roles/create`} - render={() => <CreatePermissionRole />} + render={() => <CreateRepositoryRole disabled={false} />} /> <ExtensionPoint name="config.route" diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js new file mode 100644 index 0000000000..55c9eac10d --- /dev/null +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -0,0 +1,75 @@ +// @flow +import React from "react"; +import RepositoryRoleForm from "./RepositoryRoleForm"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import { + createRole, + getFetchVerbsFailure, + isFetchVerbsPending +} from "../modules/roles"; +import type { Role } from "@scm-manager/ui-types"; +import { + getRepositoryRolesLink, + getRepositoryVerbsLink +} from "../../../modules/indexResource"; + +type Props = { + disabled: boolean, + repositoryRolesLink: string, + + //dispatch function + addRole: (link: string, role: Role) => void +}; + +class CreateRepositoryRole extends React.Component<Props> { + //Callback after dispatch + repositoryRoleCreated = (role: Role) => { + const { history } = this.props; + history.push("/role/" + role.name); + }; + + createRepositoryRole = (role: Role) => { + this.props.addRole(this.props.repositoryRolesLink, role, () => + this.repositoryRoleCreated(role) + ); + }; + + render() { + return ( + <> + <RepositoryRoleForm + disabled={this.props.disabled} + submitForm={role => this.createRepositoryRole(role)} + /> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const loading = isFetchVerbsPending(state); + const error = getFetchVerbsFailure(state); + const verbsLink = getRepositoryVerbsLink(state); + const repositoryRolesLink = getRepositoryRolesLink(state); + + return { + loading, + error, + verbsLink, + repositoryRolesLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + addRole: (link: string, role: Role) => { + dispatch(createRole(link, role)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("roles")(CreateRepositoryRole)); diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index 20f3faa713..0e0065ce16 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -20,13 +20,14 @@ type Props = { disabled: boolean, availableVerbs: string[], verbsLink: string, + submitForm: Role => void, // context objects t: string => string, // dispatch functions - fetchAvailableVerbs: (link: string) => void, - addRole: (link: string, role: Role) => void + fetchAvailableVerbs: (link: string) => void + // addRole: (link: string, role: Role) => void }; type State = { @@ -93,7 +94,7 @@ class RepositoryRoleForm extends React.Component<Props, State> { submit = (event: Event) => { event.preventDefault(); if (this.isValid()) { - this.props.addRole(this.props.repositoryRolesLink, this.state.role) + this.props.submitForm(this.state.role) } }; @@ -122,7 +123,7 @@ class RepositoryRoleForm extends React.Component<Props, State> { label={t("roles.create.name")} onChange={this.handleNameChange} value={role.name ? role.name : ""} - disabled={!!role.name || disabled} + disabled={disabled} /> </div> </div> @@ -163,9 +164,9 @@ const mapDispatchToProps = dispatch => { fetchAvailableVerbs: (link: string) => { dispatch(fetchAvailableVerbs(link)); }, - addRole: (link: string, role: Role) => { - createRole(link, role) - } + // addRole: (link: string, role: Role) => { + // createRole(link, role) + // } }; }; diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index e1aa171152..f39d55ab77 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -21,6 +21,7 @@ type Props = { loading: boolean, error: Error, repositoryRolesLink: string, + disabled: boolean, // dispatcher function fetchRoleByName: (string, string) => void, From 8ebd720f8ef0afbd99462e75089321609ccaf96f Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 15 May 2019 15:21:43 +0200 Subject: [PATCH 64/91] prepare RepoRoleDetailsView --- .../roles/components/PermissionRoleRow.js | 3 ++- .../roles/containers/SingleRepositoryRole.js | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/scm-ui/src/config/roles/components/PermissionRoleRow.js b/scm-ui/src/config/roles/components/PermissionRoleRow.js index ba38fc5c4e..9f06eb56de 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleRow.js +++ b/scm-ui/src/config/roles/components/PermissionRoleRow.js @@ -20,7 +20,8 @@ class PermissionRoleRow extends React.Component<Props> { render() { const { baseUrl, role } = this.props; - const to = `${baseUrl}/${encodeURIComponent(role.name)}/info`; + const singleRepoRoleUrl = baseUrl.substring(0, baseUrl.length - 1); + const to = `${singleRepoRoleUrl}/${encodeURIComponent(role.name)}/info`; return ( <tr> <td>{this.renderLink(to, role.name, !role._links.update)}</td> diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index f39d55ab77..cc4e94cdd0 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -14,9 +14,12 @@ import type { Role } from "@scm-manager/ui-types"; import {getRepositoryRolesLink} from "../../../modules/indexResource"; import {ExtensionPoint} from "@scm-manager/ui-extensions"; import {fetchRoleByName, getFetchRoleFailure, getRoleByName, isFetchRolePending} from "../modules/roles"; +import RepositoryRoleForm from "./RepositoryRoleForm"; +import {withRouter} from "react-router-dom"; +import PermissionRoleDetail from "../components/PermissionRoleDetail"; type Props = { - name: string, + roleName: string, role: Role, loading: boolean, error: Error, @@ -34,7 +37,8 @@ type Props = { class SingleRepositoryRole extends React.Component<Props> { componentDidMount() { - this.props.fetchRoleByName(this.props.repositoryRolesLink, this.props.name); + this.props.fetchRoleByName(this.props.repositoryRolesLink, this.props.roleName); + console.log(this.props.match) } stripEndingSlash = (url: string) => { @@ -76,7 +80,7 @@ class SingleRepositoryRole extends React.Component<Props> { <Page title={role.displayName}> <div className="columns"> <div className="column is-three-quarters"> - <Route path={url} exact component={() => <RepositoryRoleDetailNavLink role={role} />} /> + <Route path={url} component={() => <PermissionRoleDetail role={role} />} /> <Route path={`${url}/settings/general`} component={() => <EditRepositoryRoleNavLink role={role} />} @@ -94,14 +98,14 @@ class SingleRepositoryRole extends React.Component<Props> { } const mapStateToProps = (state, ownProps) => { - const name = ownProps.match.params.name; - const role = getRoleByName(state, name); - const loading = isFetchRolePending(state, name); - const error = getFetchRoleFailure(state, name); + const roleName = ownProps.match.params.role; + const role = getRoleByName(state, roleName); + const loading = isFetchRolePending(state, roleName); + const error = getFetchRoleFailure(state, roleName); const repositoryRolesLink = getRepositoryRolesLink(state); return { repositoryRolesLink, - name, + roleName, role, loading, error @@ -116,7 +120,7 @@ const mapDispatchToProps = dispatch => { }; }; -export default connect( +export default withRouter(connect( mapStateToProps, mapDispatchToProps -)(translate("users")(SingleRepositoryRole)); +)(translate("users")(SingleRepositoryRole))); From 42bc84a6a5117ee563ec0dcb75e3fa8cf55df266 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 15:48:02 +0200 Subject: [PATCH 65/91] add role to RepositoryPermissions ui-type and local state in CreatePermissionForm --- .../packages/ui-components/src/buttons/Button.js | 2 ++ .../packages/ui-types/src/RepositoryPermissions.js | 13 +++++++------ .../permissions/containers/CreatePermissionForm.js | 14 ++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/buttons/Button.js b/scm-ui-components/packages/ui-components/src/buttons/Button.js index 2102bb540a..5e80db1e45 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/Button.js +++ b/scm-ui-components/packages/ui-components/src/buttons/Button.js @@ -12,6 +12,8 @@ export type ButtonProps = { fullWidth?: boolean, className?: string, children?: React.Node, + + // context props classes: any }; diff --git a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js index ed3c925283..fe8cb3fd44 100644 --- a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js +++ b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js @@ -1,14 +1,15 @@ //@flow import type {Links} from "./hal"; +export type PermissionCreateEntry = { + name: string, + role?: string, + verbs: string[], + groupPermission: boolean +} + export type Permission = PermissionCreateEntry & { _links: Links }; -export type PermissionCreateEntry = { - name: string, - verbs: string[], - groupPermission: boolean -} - export type PermissionCollection = Permission[]; diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 94fc707dc1..6f66867d99 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -32,6 +32,7 @@ type Props = { type State = { name: string, + role?: string, verbs: string[], groupPermission: boolean, valid: boolean, @@ -45,6 +46,7 @@ class CreatePermissionForm extends React.Component<Props, State> { this.state = { name: "", + role: props.availableRoles[0].name, verbs: props.availableRoles[0].verbs, groupPermission: false, valid: true, @@ -137,9 +139,7 @@ class CreatePermissionForm extends React.Component<Props, State> { const { verbs, showAdvancedDialog } = this.state; - const availableRoleNames = availableRoles.map( - r => r.name - ); + const availableRoleNames = availableRoles.map(r => r.name); const matchingRole = findMatchingRoleName(availableRoles, verbs); const advancedDialog = showAdvancedDialog ? ( @@ -230,6 +230,7 @@ class CreatePermissionForm extends React.Component<Props, State> { submitAdvancedPermissionsDialog = (newVerbs: string[]) => { this.setState({ showAdvancedDialog: false, + role: undefined, verbs: newVerbs }); }; @@ -237,6 +238,7 @@ class CreatePermissionForm extends React.Component<Props, State> { submit = e => { this.props.createPermission({ name: this.state.name, + role: this.state.role, verbs: this.state.verbs, groupPermission: this.state.groupPermission }); @@ -247,6 +249,7 @@ class CreatePermissionForm extends React.Component<Props, State> { removeState = () => { this.setState({ name: "", + role: undefined, verbs: this.props.availableRoles[0].verbs, valid: true, value: undefined @@ -259,14 +262,13 @@ class CreatePermissionForm extends React.Component<Props, State> { return; } this.setState({ + role: selectedRole.name, verbs: selectedRole.verbs }); }; findAvailableRole = (roleName: string) => { - return this.props.availableRoles.find( - role => role.name === roleName - ); + return this.props.availableRoles.find(role => role.name === roleName); }; } From a028e874b4f49cf7c65024281c0f2369c592b1c7 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 15:48:38 +0200 Subject: [PATCH 66/91] add missing de translation --- scm-ui/public/locales/de/config.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index cfa610042f..cc1847b184 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -10,7 +10,6 @@ "navLink": "Berechtigungsrollen", "title": "Berechtigungsrollen", "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", - "system": "System", "createButton": "Berechtigungsrolle erstellen", "form": { "subtitle": "Berechtigungsrolle bearbeiten", @@ -19,6 +18,10 @@ "submit": "Speichern" } }, + "role": { + "name": "Name", + "system": "System" + }, "config-form": { "submit": "Speichern", "submit-success-notification": "Einstellungen wurden erfolgreich geändert!", From 52b01e9a9d266f9d5759ce132789a630143d9e85 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 15:52:16 +0200 Subject: [PATCH 67/91] add missing props and definitions --- .../permissions/containers/Permissions.js | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 02c43d254c..eff984ad5d 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -19,7 +19,9 @@ import { getDeletePermissionsFailure, getModifyPermissionsFailure, modifyPermissionReset, - deletePermissionReset, getAvailableRepositoryRoles, getAvailableRepositoryVerbs + deletePermissionReset, + getAvailableRepositoryRoles, + getAvailableRepositoryVerbs } from "../modules/permissions"; import { Loading, @@ -38,11 +40,14 @@ import CreatePermissionForm from "./CreatePermissionForm"; import type { History } from "history"; import { getPermissionsLink } from "../../modules/repos"; import { - getGroupAutoCompleteLink, getRepositoryRolesLink, getRepositoryVerbsLink, + getGroupAutoCompleteLink, + getRepositoryRolesLink, + getRepositoryVerbsLink, getUserAutoCompleteLink } from "../../../modules/indexResource"; type Props = { + availablePermissions: boolean, availableRepositoryRoles: RepositoryRole[], availableVerbs: string[], namespace: string, @@ -59,7 +64,10 @@ type Props = { userAutoCompleteLink: string, //dispatch functions - fetchAvailablePermissionsIfNeeded: () => void, + fetchAvailablePermissionsIfNeeded: ( + repositoryRolesLink: string, + repositoryVerbsLink: string + ) => void, fetchPermissions: (link: string, namespace: string, repoName: string) => void, createPermission: ( link: string, @@ -77,7 +85,6 @@ type Props = { history: History }; - class Permissions extends React.Component<Props> { componentDidMount() { const { @@ -251,8 +258,16 @@ const mapDispatchToProps = dispatch => { fetchPermissions: (link: string, namespace: string, repoName: string) => { dispatch(fetchPermissions(link, namespace, repoName)); }, - fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => { - dispatch(fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink)); + fetchAvailablePermissionsIfNeeded: ( + repositoryRolesLink: string, + repositoryVerbsLink: string + ) => { + dispatch( + fetchAvailablePermissionsIfNeeded( + repositoryRolesLink, + repositoryVerbsLink + ) + ); }, createPermission: ( link: string, From 6c627bfe005fce88226e11499be1316ec6ebd141 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 15 May 2019 16:41:14 +0200 Subject: [PATCH 68/91] update RepositoryRoleDetailsView --- .../roles/components/PermissionRoleDetail.js | 29 --------- .../roles/components/PermissionRoleDetails.js | 37 +++++++++++ .../components/PermissionRoleDetailsTable.js | 65 +++++++++++++++++++ .../roles/containers/SingleRepositoryRole.js | 55 +++++++++------- 4 files changed, 135 insertions(+), 51 deletions(-) delete mode 100644 scm-ui/src/config/roles/components/PermissionRoleDetail.js create mode 100644 scm-ui/src/config/roles/components/PermissionRoleDetails.js create mode 100644 scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetail.js b/scm-ui/src/config/roles/components/PermissionRoleDetail.js deleted file mode 100644 index 496443eef2..0000000000 --- a/scm-ui/src/config/roles/components/PermissionRoleDetail.js +++ /dev/null @@ -1,29 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import type { Role } from "@scm-manager/ui-types"; -import SystemRoleTag from "./SystemRoleTag"; - -type Props = { - role: Role, - - // context props - t: string => string, -}; - -class PermissionRoleDetail extends React.Component<Props> { - render() { - const { role, t } = this.props; - - return ( - <div className="media"> - <div className="media-content subtitle"> - <strong>{t("role.name")}:</strong> {role.name}{" "} - <SystemRoleTag system={!role._links.update} /> - </div> - </div> - ); - } -} - -export default translate("config")(PermissionRoleDetail); diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js new file mode 100644 index 0000000000..f4793cf067 --- /dev/null +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -0,0 +1,37 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Role } from "@scm-manager/ui-types"; +import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint"; +import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable"; + +type Props = { + role: Role, + + // context props + t: string => string, +}; + +class PermissionRoleDetails extends React.Component<Props> { + render() { + const { role } = this.props; + + return ( + + <div> + <PermissionRoleDetailsTable role={role}/> + <hr/> + <div className="content"> + <ExtensionPoint + name="roles.repositoryRole-details.information" + renderAll={true} + props={{ role }} + /> + </div> + </div> + + ); + } +} + +export default translate("roles")(PermissionRoleDetails); diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js new file mode 100644 index 0000000000..a66b080adf --- /dev/null +++ b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js @@ -0,0 +1,65 @@ +//@flow +import React from "react"; +import type { Role } from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import { compose } from "redux"; +import injectSheet from "react-jss"; + +type Props = { + role: Role, + // context props + t: string => string +}; + +const styles = { + spacing: { + padding: "0 !important" + } +}; + +class PermissionRoleDetailsTable extends React.Component<Props> { + render() { + const { role, t } = this.props; + return ( + <table className="table content"> + <tbody> + <tr> + <th>{t("repositoryRole.name")}</th> + <td>{role.name}</td> + </tr> + <tr> + <th>{t("repositoryRole.type")}</th> + <td>{role.type}</td> + </tr> + {this.renderVerbs()} + </tbody> + </table> + ); + } + + renderVerbs() { + const { role, t, classes } = this.props; + + let verbs = null; + if (role.verbs.length > 0) { + verbs = ( + <tr> + <th>{t("repositoryRole.verbs")}</th> + <td className={classes.spacing}> + <ul> + {role.verbs.map(verb => { + return <li>{verb}</li>; + })} + </ul> + </td> + </tr> + ); + } + return verbs; + } +} + +export default compose( + injectSheet(styles), + translate("roles") +)(PermissionRoleDetailsTable); diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index cc4e94cdd0..6c77bfd46b 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -1,22 +1,24 @@ //@flow import React from "react"; import { connect } from "react-redux"; -import { - Page, - Loading, - ErrorPage -} from "@scm-manager/ui-components"; +import {Loading, ErrorPage, Title} from "@scm-manager/ui-components"; import { Route } from "react-router"; import type { History } from "history"; -import { EditRepositoryRoleNavLink, RepositoryRoleDetailNavLink } from "../../components/navLinks"; +import { + EditRepositoryRoleNavLink, +} from "../../components/navLinks"; import { translate } from "react-i18next"; import type { Role } from "@scm-manager/ui-types"; -import {getRepositoryRolesLink} from "../../../modules/indexResource"; -import {ExtensionPoint} from "@scm-manager/ui-extensions"; -import {fetchRoleByName, getFetchRoleFailure, getRoleByName, isFetchRolePending} from "../modules/roles"; -import RepositoryRoleForm from "./RepositoryRoleForm"; -import {withRouter} from "react-router-dom"; -import PermissionRoleDetail from "../components/PermissionRoleDetail"; +import { getRepositoryRolesLink } from "../../../modules/indexResource"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { + fetchRoleByName, + getFetchRoleFailure, + getRoleByName, + isFetchRolePending +} from "../modules/roles"; +import { withRouter } from "react-router-dom"; +import PermissionRoleDetail from "../components/PermissionRoleDetails"; type Props = { roleName: string, @@ -37,8 +39,11 @@ type Props = { class SingleRepositoryRole extends React.Component<Props> { componentDidMount() { - this.props.fetchRoleByName(this.props.repositoryRolesLink, this.props.roleName); - console.log(this.props.match) + this.props.fetchRoleByName( + this.props.repositoryRolesLink, + this.props.roleName + ); + console.log(this.props.match); } stripEndingSlash = (url: string) => { @@ -77,22 +82,26 @@ class SingleRepositoryRole extends React.Component<Props> { }; return ( - <Page title={role.displayName}> + <> + <Title title={t("repositoryRoles.title")}/> <div className="columns"> <div className="column is-three-quarters"> - <Route path={url} component={() => <PermissionRoleDetail role={role} />} /> + <Route + path={url} + component={() => <PermissionRoleDetail role={role} />} + /> <Route path={`${url}/settings/general`} component={() => <EditRepositoryRoleNavLink role={role} />} /> <ExtensionPoint - name="user.route" + name="roles.route" props={extensionProps} renderAll={true} /> </div> </div> - </Page> + </> ); } } @@ -120,7 +129,9 @@ const mapDispatchToProps = dispatch => { }; }; -export default withRouter(connect( - mapStateToProps, - mapDispatchToProps -)(translate("users")(SingleRepositoryRole))); +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(translate("config")(SingleRepositoryRole)) +); From 60cf070c448b7ff8052ca263597128dc8b010a4b Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 15 May 2019 16:57:28 +0200 Subject: [PATCH 69/91] refactor / enable routing after createRepoRole --- scm-ui/src/config/containers/Config.js | 2 +- .../config/roles/components/PermissionRoleDetails.js | 2 +- .../roles/components/PermissionRoleDetailsTable.js | 2 +- .../config/roles/containers/CreateRepositoryRole.js | 12 ++++++------ .../config/roles/containers/RepositoryRoleForm.js | 8 +------- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 8c5a3b1fcc..63173c0b9b 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -66,7 +66,7 @@ class Config extends React.Component<Props> { /> <Route path={`${url}/roles/create`} - render={() => <CreateRepositoryRole disabled={false} />} + render={() => <CreateRepositoryRole disabled={false} history={this.props.history} />} /> <ExtensionPoint name="config.route" diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index f4793cf067..92dab08b86 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -34,4 +34,4 @@ class PermissionRoleDetails extends React.Component<Props> { } } -export default translate("roles")(PermissionRoleDetails); +export default translate("config")(PermissionRoleDetails); diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js index a66b080adf..67d752fb8c 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js @@ -61,5 +61,5 @@ class PermissionRoleDetailsTable extends React.Component<Props> { export default compose( injectSheet(styles), - translate("roles") + translate("config") )(PermissionRoleDetailsTable); diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index 55c9eac10d..6592ff566a 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -19,14 +19,14 @@ type Props = { repositoryRolesLink: string, //dispatch function - addRole: (link: string, role: Role) => void + addRole: (link: string, role: Role, callback?: () => void) => void }; class CreateRepositoryRole extends React.Component<Props> { - //Callback after dispatch + repositoryRoleCreated = (role: Role) => { const { history } = this.props; - history.push("/role/" + role.name); + history.push("/config/role/" + role.name); }; createRepositoryRole = (role: Role) => { @@ -63,8 +63,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - addRole: (link: string, role: Role) => { - dispatch(createRole(link, role)); + addRole: (link: string, role: Role, callback?: () => void) => { + dispatch(createRole(link, role, callback)); } }; }; @@ -72,4 +72,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("roles")(CreateRepositoryRole)); +)(translate("config")(CreateRepositoryRole)); diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index 0e0065ce16..e35c2f2c2f 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -6,7 +6,6 @@ import type { Role } from "@scm-manager/ui-types"; import { InputField, SubmitButton } from "@scm-manager/ui-components"; import PermissionCheckbox from "../../../repos/permissions/components/PermissionCheckbox"; import { - createRole, fetchAvailableVerbs, getFetchVerbsFailure, getVerbsFromState, @@ -27,7 +26,6 @@ type Props = { // dispatch functions fetchAvailableVerbs: (link: string) => void - // addRole: (link: string, role: Role) => void }; type State = { @@ -107,7 +105,6 @@ class RepositoryRoleForm extends React.Component<Props, State> { : availableVerbs.map(verb => ( <PermissionCheckbox key={verb} - // disabled={readOnly} name={verb} checked={role.verbs.includes(verb)} onChange={this.handleVerbChange} @@ -164,13 +161,10 @@ const mapDispatchToProps = dispatch => { fetchAvailableVerbs: (link: string) => { dispatch(fetchAvailableVerbs(link)); }, - // addRole: (link: string, role: Role) => { - // createRole(link, role) - // } }; }; export default connect( mapStateToProps, mapDispatchToProps -)(translate("roles")(RepositoryRoleForm)); +)(translate("config")(RepositoryRoleForm)); From 3c44f9910f30440b39fb0f3db21a0d1c6c1c5cbc Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 17:15:00 +0200 Subject: [PATCH 70/91] add translation for custom role selection --- scm-ui/public/locales/de/repos.json | 1 + scm-ui/public/locales/en/repos.json | 1 + 2 files changed, 2 insertions(+) diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index f4ee071613..4ba7a725e6 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -119,6 +119,7 @@ "error-subtitle": "Unbekannter Fehler bei Berechtigung", "name": "Benutzer oder Gruppe", "role": "Rolle", + "custom": "CUSTOM", "permissions": "Berechtigung", "group-permission": "Gruppenberechtigung", "user-permission": "Benutzerberechtigung", diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 7344547d49..9a2e83f983 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -122,6 +122,7 @@ "error-subtitle": "Unknown permissions error", "name": "User or group", "role": "Role", + "custom": "CUSTOM", "permissions": "Permissions", "group-permission": "Group Permission", "user-permission": "User Permission", From 394c1f53d47ba0bdf2d5b91f18e5d70d9a8b777e Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 15 May 2019 17:30:42 +0200 Subject: [PATCH 71/91] create EditRepoRoles --- .../roles/components/PermissionRoleDetails.js | 6 +- .../roles/containers/EditRepositoryRole.js | 71 +++++++++++++++++++ .../roles/containers/RepositoryRoleForm.js | 3 + .../roles/containers/SingleRepositoryRole.js | 37 +++++----- 4 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 scm-ui/src/config/roles/containers/EditRepositoryRole.js diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index 92dab08b86..8ca21b1ac3 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -4,9 +4,11 @@ import { translate } from "react-i18next"; import type { Role } from "@scm-manager/ui-types"; import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint"; import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable"; +import {Button, Subtitle} from "@scm-manager/ui-components"; type Props = { role: Role, + url: string, // context props t: string => string, @@ -14,13 +16,15 @@ type Props = { class PermissionRoleDetails extends React.Component<Props> { render() { - const { role } = this.props; + const { role, url } = this.props; return ( <div> <PermissionRoleDetailsTable role={role}/> <hr/> + <Subtitle subtitle={"repositoryRoles.edit"}/> + <Button label={"test"} link={`${url}/edit`} color="primary" /> <div className="content"> <ExtensionPoint name="roles.repositoryRole-details.information" diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js new file mode 100644 index 0000000000..f16db79dda --- /dev/null +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -0,0 +1,71 @@ +// @flow +import React from "react"; +import RepositoryRoleForm from "./RepositoryRoleForm"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import { + getModifyRoleFailure, + isModifyRolePending, + modifyRole, +} from "../modules/roles"; +import type { Role } from "@scm-manager/ui-types"; + +type Props = { + disabled: boolean, + role: Role, + repositoryRolesLink: string, + + //dispatch function + updateRole: (link: string, role: Role, callback?: () => void) => void +}; + + + +class EditRepositoryRole extends React.Component<Props> { + + repositoryRoleUpdated = (role: Role) => { + const { history } = this.props; + history.push("/config/role/" + role.name); + }; + + updateRepositoryRole = (role: Role) => { + this.props.updateRole(role, () => + this.repositoryRoleUpdated(role) + ); + }; + + render() { + return ( + <> + <RepositoryRoleForm + disabled={false} + role={this.props.role} + submitForm={role => this.updateRepositoryRole(role)} + /> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const loading = isModifyRolePending(state); + const error = getModifyRoleFailure(state); + + return { + loading, + error, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + updateRole: (role: Role, callback?: () => void) => { + dispatch(modifyRole(role, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("config")(EditRepositoryRole)); diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index e35c2f2c2f..852e097e4a 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -49,6 +49,9 @@ class RepositoryRoleForm extends React.Component<Props, State> { componentDidMount() { const { fetchAvailableVerbs, verbsLink} = this.props; fetchAvailableVerbs(verbsLink); + if (this.props.role) { + this.setState({role: this.props.role}) + } } isFalsy(value) { diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index 6c77bfd46b..6b4f48819a 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -1,12 +1,10 @@ //@flow import React from "react"; import { connect } from "react-redux"; -import {Loading, ErrorPage, Title} from "@scm-manager/ui-components"; +import { Loading, ErrorPage, Title } from "@scm-manager/ui-components"; import { Route } from "react-router"; import type { History } from "history"; -import { - EditRepositoryRoleNavLink, -} from "../../components/navLinks"; +import { EditRepositoryRoleNavLink } from "../../components/navLinks"; import { translate } from "react-i18next"; import type { Role } from "@scm-manager/ui-types"; import { getRepositoryRolesLink } from "../../../modules/indexResource"; @@ -19,6 +17,8 @@ import { } from "../modules/roles"; import { withRouter } from "react-router-dom"; import PermissionRoleDetail from "../components/PermissionRoleDetails"; +import EditRepositoryRole from "./EditRepositoryRole"; +import Switch from "react-router-dom/es/Switch"; type Props = { roleName: string, @@ -83,22 +83,23 @@ class SingleRepositoryRole extends React.Component<Props> { return ( <> - <Title title={t("repositoryRoles.title")}/> + <Title title={t("repositoryRoles.title")} /> <div className="columns"> <div className="column is-three-quarters"> - <Route - path={url} - component={() => <PermissionRoleDetail role={role} />} - /> - <Route - path={`${url}/settings/general`} - component={() => <EditRepositoryRoleNavLink role={role} />} - /> - <ExtensionPoint - name="roles.route" - props={extensionProps} - renderAll={true} - /> + <Route + path={url} + component={() => <PermissionRoleDetail role={role} url={url} />} + /> + <Route + path={`${url}`} + exact + component={() => <EditRepositoryRole role={role} />} + /> + <ExtensionPoint + name="roles.route" + props={extensionProps} + renderAll={true} + /> </div> </div> </> From 32d7eb87017417ccd6347077cec7c57f247e51e7 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 15 May 2019 17:30:48 +0200 Subject: [PATCH 72/91] role-ready repositoryPermissionView --- .../ui-types/src/RepositoryPermissions.js | 2 +- .../containers/AdvancedPermissionsDialog.js | 6 +- .../containers/CreatePermissionForm.js | 39 +++++---- .../containers/SinglePermission.js | 80 +++++++++---------- .../repos/permissions/modules/permissions.js | 32 -------- 5 files changed, 61 insertions(+), 98 deletions(-) diff --git a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js index fe8cb3fd44..14a2298fbe 100644 --- a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js +++ b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js @@ -4,7 +4,7 @@ import type {Links} from "./hal"; export type PermissionCreateEntry = { name: string, role?: string, - verbs: string[], + verbs?: string[], groupPermission: boolean } diff --git a/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js b/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js index cc662e5f06..d15b9112b9 100644 --- a/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js +++ b/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js @@ -26,9 +26,11 @@ class AdvancedPermissionsDialog extends React.Component<Props, State> { const verbs = {}; props.availableVerbs.forEach( - verb => (verbs[verb] = props.selectedVerbs.includes(verb)) + verb => + (verbs[verb] = props.selectedVerbs + ? props.selectedVerbs.includes(verb) + : false) ); - this.state = { verbs }; } diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 6f66867d99..8ee1e7edbb 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -1,6 +1,12 @@ // @flow import React from "react"; import { translate } from "react-i18next"; +import type { + RepositoryRole, + PermissionCollection, + PermissionCreateEntry, + SelectValue +} from "@scm-manager/ui-types"; import { Autocomplete, SubmitButton, @@ -8,32 +14,27 @@ import { LabelWithHelpIcon, Radio } from "@scm-manager/ui-components"; -import RoleSelector from "../components/RoleSelector"; -import type { - RepositoryRole, - PermissionCollection, - PermissionCreateEntry, - SelectValue -} from "@scm-manager/ui-types"; import * as validator from "../components/permissionValidation"; -import { findMatchingRoleName } from "../modules/permissions"; +import RoleSelector from "../components/RoleSelector"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; type Props = { - t: string => string, availableRoles: RepositoryRole[], availableVerbs: string[], createPermission: (permission: PermissionCreateEntry) => void, loading: boolean, currentPermissions: PermissionCollection, groupAutoCompleteLink: string, - userAutoCompleteLink: string + userAutoCompleteLink: string, + + // Context props + t: string => string }; type State = { name: string, role?: string, - verbs: string[], + verbs?: string[], groupPermission: boolean, valid: boolean, value?: SelectValue, @@ -47,7 +48,7 @@ class CreatePermissionForm extends React.Component<Props, State> { this.state = { name: "", role: props.availableRoles[0].name, - verbs: props.availableRoles[0].verbs, + verbs: undefined, groupPermission: false, valid: true, value: undefined, @@ -92,6 +93,7 @@ class CreatePermissionForm extends React.Component<Props, State> { }); }); } + renderAutocompletionField = () => { const { t } = this.props; if (this.state.groupPermission) { @@ -136,11 +138,9 @@ class CreatePermissionForm extends React.Component<Props, State> { render() { const { t, availableRoles, availableVerbs, loading } = this.props; - - const { verbs, showAdvancedDialog } = this.state; + const { role, verbs, showAdvancedDialog } = this.state; const availableRoleNames = availableRoles.map(r => r.name); - const matchingRole = findMatchingRoleName(availableRoles, verbs); const advancedDialog = showAdvancedDialog ? ( <AdvancedPermissionsDialog @@ -189,7 +189,7 @@ class CreatePermissionForm extends React.Component<Props, State> { label={t("permission.role")} helpText={t("permission.help.roleHelpText")} handleRoleChange={this.handleRoleChange} - role={matchingRole} + role={role} /> </div> <div className="column"> @@ -249,8 +249,8 @@ class CreatePermissionForm extends React.Component<Props, State> { removeState = () => { this.setState({ name: "", - role: undefined, - verbs: this.props.availableRoles[0].verbs, + role: this.props.availableRoles[0].name, + verbs: undefined, valid: true, value: undefined }); @@ -262,8 +262,7 @@ class CreatePermissionForm extends React.Component<Props, State> { return; } this.setState({ - role: selectedRole.name, - verbs: selectedRole.verbs + role: selectedRole.name }); }; diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js index c90d6ab968..5fed0084a7 100644 --- a/scm-ui/src/repos/permissions/containers/SinglePermission.js +++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js @@ -1,16 +1,12 @@ // @flow import React from "react"; -import type { - RepositoryRole, - Permission -} from "@scm-manager/ui-types"; +import type { RepositoryRole, Permission } from "@scm-manager/ui-types"; import { translate } from "react-i18next"; import { modifyPermission, isModifyPermissionPending, deletePermission, - isDeletePermissionPending, - findMatchingRoleName + isDeletePermissionPending } from "../modules/permissions"; import { connect } from "react-redux"; import type { History } from "history"; @@ -47,7 +43,6 @@ type Props = { }; type State = { - role: string, permission: Permission, showAdvancedDialog: boolean }; @@ -69,39 +64,34 @@ class SinglePermission extends React.Component<Props, State> { constructor(props: Props) { super(props); - const defaultPermission = props.availableRoles - ? props.availableRoles[0] + const defaultPermission = props.availableRepositoryRoles + ? props.availableRepositoryRoles[0] : {}; this.state = { permission: { name: "", + role: undefined, verbs: defaultPermission.verbs, groupPermission: false, _links: {} }, - role: defaultPermission.name, showAdvancedDialog: false }; } componentDidMount() { - const { availableRepositoryRoles, permission } = this.props; - - const matchingRole = findMatchingRoleName( - availableRepositoryRoles, - permission.verbs - ); + const { permission } = this.props; if (permission) { this.setState({ permission: { name: permission.name, + role: permission.role, verbs: permission.verbs, groupPermission: permission.groupPermission, _links: permission._links - }, - role: matchingRole + } }); } } @@ -115,7 +105,7 @@ class SinglePermission extends React.Component<Props, State> { }; render() { - const { role, permission, showAdvancedDialog } = this.state; + const { permission, showAdvancedDialog } = this.state; const { t, availableRepositoryRoles, @@ -125,18 +115,17 @@ class SinglePermission extends React.Component<Props, State> { repoName, classes } = this.props; - const availableRoleNames = !!availableRepositoryRoles && availableRepositoryRoles.map( - r => r.name - ); + const availableRoleNames = + !!availableRepositoryRoles && availableRepositoryRoles.map(r => r.name); const readOnly = !this.mayChangePermissions(); const roleSelector = readOnly ? ( - <td>{role}</td> + <td>{permission.role ? permission.role : t("permission.custom")}</td> ) : ( <td> <RoleSelector handleRoleChange={this.handleRoleChange} availableRoles={availableRoleNames} - role={role} + role={permission.role} loading={loading} /> </td> @@ -154,9 +143,15 @@ class SinglePermission extends React.Component<Props, State> { const iconType = permission && permission.groupPermission ? ( - <i title={t("permission.group")} className={classNames("fas fa-user-friends", classes.iconColor)} /> + <i + title={t("permission.group")} + className={classNames("fas fa-user-friends", classes.iconColor)} + /> ) : ( - <i title={t("permission.user")} className={classNames("fas fa-user", classes.iconColor)} /> + <i + title={t("permission.user")} + className={classNames("fas fa-user", classes.iconColor)} + /> ); return ( @@ -199,42 +194,41 @@ class SinglePermission extends React.Component<Props, State> { submitAdvancedPermissionsDialog = (newVerbs: string[]) => { const { permission } = this.state; - const newRole = findMatchingRoleName( - this.props.availableRoles, - newVerbs - ); this.setState( { showAdvancedDialog: false, - permission: { ...permission, verbs: newVerbs }, - role: newRole + permission: { ...permission, role: undefined, verbs: newVerbs } }, - () => this.modifyPermission(newVerbs) + () => this.modifyPermissionVerbs(newVerbs) ); }; handleRoleChange = (role: string) => { - const selectedRole = this.findAvailableRole(role); + const { permission } = this.state; this.setState( { - permission: { - ...this.state.permission, - verbs: selectedRole.verbs - }, - role: role + permission: { ...permission, role: role, verbs: undefined } }, - () => this.modifyPermission(selectedRole.verbs) + () => this.modifyPermissionRole(role) ); }; findAvailableRole = (roleName: string) => { const { availableRepositoryRoles } = this.props; - return availableRepositoryRoles.find( - role => role.name === roleName + return availableRepositoryRoles.find(role => role.name === roleName); + }; + + modifyPermissionRole = (role: string) => { + let permission = this.state.permission; + permission.role = role; + this.props.modifyPermission( + permission, + this.props.namespace, + this.props.repoName ); }; - modifyPermission = (verbs: string[]) => { + modifyPermissionVerbs = (verbs: string[]) => { let permission = this.state.permission; permission.verbs = verbs; this.props.modifyPermission( diff --git a/scm-ui/src/repos/permissions/modules/permissions.js b/scm-ui/src/repos/permissions/modules/permissions.js index 2aa9d139c5..abd25eb459 100644 --- a/scm-ui/src/repos/permissions/modules/permissions.js +++ b/scm-ui/src/repos/permissions/modules/permissions.js @@ -739,35 +739,3 @@ export function getModifyPermissionsFailure( } return null; } - -export function findMatchingRoleName( - availableRoles: RepositoryRole[], - verbs: string[] -) { - if (!verbs) { - return ""; - } - const matchingRole = - !!availableRoles && - availableRoles.find(role => { - return equalVerbs(role.verbs, verbs); - }); - - if (matchingRole) { - return matchingRole.name; - } else { - return ""; - } -} - -function equalVerbs(verbs1: string[], verbs2: string[]) { - if (!verbs1 || !verbs2) { - return false; - } - - if (verbs1.length !== verbs2.length) { - return false; - } - - return verbs1.every(verb => verbs2.includes(verb)); -} From a726005730c42f0f1905a0a5929fad12e1d1b744 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 08:38:26 +0200 Subject: [PATCH 73/91] update Translations --- scm-ui/public/locales/de/config.json | 12 +++++++++++- scm-ui/public/locales/en/config.json | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index cfa610042f..eff3efa718 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -6,12 +6,22 @@ "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Einstellungen Fehler" }, - "roles": { + "repositoryRole": { "navLink": "Berechtigungsrollen", "title": "Berechtigungsrollen", "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", "system": "System", "createButton": "Berechtigungsrolle erstellen", + "name": "Name", + "type": "Typ", + "verbs": "Verben", + "button": { + "edit": "Bearbeiten" + }, + "create": { + "name": "Name" + }, + "edit": "Berechtigungsrolle bearbeiten", "form": { "subtitle": "Berechtigungsrolle bearbeiten", "name": "Name", diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index e4ecc39874..bdb245544a 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -6,11 +6,22 @@ "errorTitle": "Error", "errorSubtitle": "Unknown Config Error" }, - "roles": { + "repositoryRole": { "navLink": "Permission Roles", "title": "Permission Roles", "noPermissionRoles": "No permission roles found.", + "system": "System", "createButton": "Create Permission Role", + "name": "Name", + "type": "Type", + "verbs": "Verbs", + "edit": "Edit Permission Role", + "button": { + "edit": "Edit" + }, + "create": { + "name": "Name" + }, "form": { "subtitle": "Edit Permission Role", "name": "Name", From 8fbe20afd6890a78446c433c53fece36d44f97fc Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 08:55:31 +0200 Subject: [PATCH 74/91] update EditRepositoryRole / fix Translationkeys --- scm-ui/src/config/containers/Config.js | 4 +- .../roles/components/PermissionRoleDetails.js | 31 +++++++++++----- .../roles/components/PermissionRoleTable.js | 2 +- .../roles/containers/CreateRepositoryRole.js | 2 +- .../roles/containers/EditRepositoryRole.js | 6 +-- .../roles/containers/RepositoryRoleForm.js | 14 +++---- .../roles/containers/RepositoryRoles.js | 8 ++-- .../roles/containers/SingleRepositoryRole.js | 37 +++++++++---------- 8 files changed, 57 insertions(+), 47 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 63173c0b9b..61e5931e24 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -57,7 +57,7 @@ class Config extends React.Component<Props> { <Route path={url} exact component={GlobalConfig} /> <Route path={`${url}/role/:role`} - render={() => <SingleRepositoryRole baseUrl={`${url}/roles`} />} + render={() => <SingleRepositoryRole baseUrl={`${url}/roles`} history={this.props.history} />} /> <Route path={`${url}/roles`} @@ -83,7 +83,7 @@ class Config extends React.Component<Props> { /> <NavLink to={`${url}/roles`} - label={t("roles.navLink")} + label={t("repositoryRole.navLink")} activeWhenMatch={this.matchesRoles} /> <ExtensionPoint diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index 8ca21b1ac3..fa68a66d25 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -4,36 +4,47 @@ import { translate } from "react-i18next"; import type { Role } from "@scm-manager/ui-types"; import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint"; import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable"; -import {Button, Subtitle} from "@scm-manager/ui-components"; +import { Button, Subtitle } from "@scm-manager/ui-components"; type Props = { role: Role, url: string, // context props - t: string => string, + t: string => string }; class PermissionRoleDetails extends React.Component<Props> { + renderEditButton() { + const { t, url } = this.props; + if (!!this.props.role._links.update) { + return ( + <Button + label={t("repositoryRole.button.edit")} + link={`${url}/edit`} + color="primary" + /> + ); + } + return null; + } + render() { - const { role, url } = this.props; + const { role } = this.props; return ( - <div> - <PermissionRoleDetailsTable role={role}/> - <hr/> - <Subtitle subtitle={"repositoryRoles.edit"}/> - <Button label={"test"} link={`${url}/edit`} color="primary" /> + <PermissionRoleDetailsTable role={role} /> + <hr /> + {this.renderEditButton()} <div className="content"> <ExtensionPoint - name="roles.repositoryRole-details.information" + name="repositoryRole.role-details.information" renderAll={true} props={{ role }} /> </div> </div> - ); } } diff --git a/scm-ui/src/config/roles/components/PermissionRoleTable.js b/scm-ui/src/config/roles/components/PermissionRoleTable.js index 895883d8c4..a585a84bca 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleTable.js @@ -18,7 +18,7 @@ class PermissionRoleTable extends React.Component<Props> { <table className="card-table table is-hoverable is-fullwidth"> <thead> <tr> - <th>{t("roles.form.name")}</th> + <th>{t("repositoryRole.form.name")}</th> </tr> </thead> <tbody> diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index 6592ff566a..e77df73deb 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -26,7 +26,7 @@ class CreateRepositoryRole extends React.Component<Props> { repositoryRoleCreated = (role: Role) => { const { history } = this.props; - history.push("/config/role/" + role.name); + history.push("/config/role/" + role.name + "/info"); }; createRepositoryRole = (role: Role) => { diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index f16db79dda..98599863fc 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -25,7 +25,7 @@ class EditRepositoryRole extends React.Component<Props> { repositoryRoleUpdated = (role: Role) => { const { history } = this.props; - history.push("/config/role/" + role.name); + history.push("/config/roles"); }; updateRepositoryRole = (role: Role) => { @@ -38,13 +38,13 @@ class EditRepositoryRole extends React.Component<Props> { return ( <> <RepositoryRoleForm - disabled={false} + nameDisabled={true} role={this.props.role} submitForm={role => this.updateRepositoryRole(role)} /> </> ); - } + }w } const mapStateToProps = (state, ownProps) => { diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index 852e097e4a..3515cddcfb 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -16,7 +16,7 @@ import {getRepositoryRolesLink, getRepositoryVerbsLink} from "../../../modules/i type Props = { role?: Role, loading?: boolean, - disabled: boolean, + nameDisabled: boolean, availableVerbs: string[], verbsLink: string, submitForm: Role => void, @@ -63,7 +63,7 @@ class RepositoryRoleForm extends React.Component<Props, State> { return !( this.isFalsy(role) || this.isFalsy(role.name) || - this.isFalsy(role.verbs) + this.isFalsy(role.verbs.length > 0) ); }; @@ -100,7 +100,7 @@ class RepositoryRoleForm extends React.Component<Props, State> { }; render() { - const { loading, availableVerbs, disabled, t } = this.props; + const { loading, availableVerbs, nameDisabled, t } = this.props; const { role } = this.state; const verbSelectBoxes = !availableVerbs @@ -120,10 +120,10 @@ class RepositoryRoleForm extends React.Component<Props, State> { <div className="column"> <InputField name="name" - label={t("roles.create.name")} + label={t("repositoryRole.create.name")} onChange={this.handleNameChange} value={role.name ? role.name : ""} - disabled={disabled} + disabled={nameDisabled} /> </div> </div> @@ -133,8 +133,8 @@ class RepositoryRoleForm extends React.Component<Props, State> { <div className="column"> <SubmitButton loading={loading} - label={t("roles.create.submit")} - disabled={disabled || !this.isValid()} + label={t("repositoryRole.form.submit")} + disabled={!this.isValid()} /> </div> </div> diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index 5a613e1fcd..35f2888e89 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -56,7 +56,7 @@ class RepositoryRoles extends React.Component<Props> { return ( <div> - <Title title={t("roles.title")} /> + <Title title={t("repositoryRole.title")} /> {this.renderPermissionsTable()} {this.renderCreateButton()} </div> @@ -69,19 +69,19 @@ class RepositoryRoles extends React.Component<Props> { return ( <> <PermissionRoleTable baseUrl={baseUrl} roles={roles} /> - <LinkPaginator collection={list} page={page} /> + <LinkPaginator collection={list} page={page} /> </> ); } return ( - <Notification type="info">{t("roles.noPermissionRoles")}</Notification> + <Notification type="info">{t("repositoryRole.noPermissionRoles")}</Notification> ); } renderCreateButton() { const { canAddRoles, baseUrl, t } = this.props; if (canAddRoles) { - return <CreateButton label={t("roles.createButton")} link={`${baseUrl}/create`} />; + return <CreateButton label={t("repositoryRole.createButton")} link={`${baseUrl}/create`} />; } return null; } diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index 6b4f48819a..247021c855 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -4,7 +4,6 @@ import { connect } from "react-redux"; import { Loading, ErrorPage, Title } from "@scm-manager/ui-components"; import { Route } from "react-router"; import type { History } from "history"; -import { EditRepositoryRoleNavLink } from "../../components/navLinks"; import { translate } from "react-i18next"; import type { Role } from "@scm-manager/ui-types"; import { getRepositoryRolesLink } from "../../../modules/indexResource"; @@ -18,7 +17,6 @@ import { import { withRouter } from "react-router-dom"; import PermissionRoleDetail from "../components/PermissionRoleDetails"; import EditRepositoryRole from "./EditRepositoryRole"; -import Switch from "react-router-dom/es/Switch"; type Props = { roleName: string, @@ -43,7 +41,6 @@ class SingleRepositoryRole extends React.Component<Props> { this.props.repositoryRolesLink, this.props.roleName ); - console.log(this.props.match); } stripEndingSlash = (url: string) => { @@ -67,7 +64,7 @@ class SingleRepositoryRole extends React.Component<Props> { subtitle={t("singleUser.errorSubtitle")} error={error} /> - ); + );0 } if (!role || loading) { @@ -83,23 +80,25 @@ class SingleRepositoryRole extends React.Component<Props> { return ( <> - <Title title={t("repositoryRoles.title")} /> + <Title title={t("repositoryRole.title")} /> <div className="columns"> <div className="column is-three-quarters"> - <Route - path={url} - component={() => <PermissionRoleDetail role={role} url={url} />} - /> - <Route - path={`${url}`} - exact - component={() => <EditRepositoryRole role={role} />} - /> - <ExtensionPoint - name="roles.route" - props={extensionProps} - renderAll={true} - /> + <Route + path={`${url}/info`} + component={() => ( + <PermissionRoleDetail role={role} url={url} /> + )} + /> + <Route + path={`${url}/edit`} + exact + component={() => <EditRepositoryRole role={role} history={this.props.history} />} + /> + <ExtensionPoint + name="roles.route" + props={extensionProps} + renderAll={true} + /> </div> </div> </> From 45733db6fa294623257c828db4b72f06d2318b65 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 08:56:17 +0200 Subject: [PATCH 75/91] fix Translationkeys --- scm-ui/src/config/roles/containers/SingleRepositoryRole.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index 247021c855..d42d36e173 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -60,8 +60,8 @@ class SingleRepositoryRole extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("singleUser.errorTitle")} - subtitle={t("singleUser.errorSubtitle")} + title={t("repositoryRole.errorTitle")} + subtitle={t("repositoryRole.errorSubtitle")} error={error} /> );0 From 7ed60eb5e1ca5944e8df2ed759daba86d9b07db3 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 09:28:39 +0200 Subject: [PATCH 76/91] remove unused classes and tests --- .../navLinks/EditRepositoryRoleNavLink.js | 28 ----------------- .../EditRepositoryRoleNavLink.test.js | 27 ---------------- .../navLinks/RepositoryRoleDetailNavLink.js | 28 ----------------- .../RepositoryRoleDetailNavLink.test.js | 31 ------------------- .../src/config/components/navLinks/index.js | 2 -- 5 files changed, 116 deletions(-) delete mode 100644 scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js delete mode 100644 scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js delete mode 100644 scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js delete mode 100644 scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js delete mode 100644 scm-ui/src/config/components/navLinks/index.js diff --git a/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js b/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js deleted file mode 100644 index 419ca330cc..0000000000 --- a/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.js +++ /dev/null @@ -1,28 +0,0 @@ -//@flow -import React from "react"; -import type { User } from "@scm-manager/ui-types"; -import { NavLink } from "@scm-manager/ui-components"; -import { translate } from "react-i18next"; - -type Props = { - user: User, - editUrl: String, - t: string => string -}; - -class EditRepositoryRoleNavLink extends React.Component<Props> { - isEditable = () => { - return this.props.user._links.update; - }; - - render() { - const { t, editUrl } = this.props; - - if (!this.isEditable()) { - return null; - } - return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} />; - } -} - -export default translate("users")(EditRepositoryRoleNavLink); diff --git a/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js b/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js deleted file mode 100644 index 7e86e9bcda..0000000000 --- a/scm-ui/src/config/components/navLinks/EditRepositoryRoleNavLink.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { shallow } from "enzyme"; -import "../../../tests/enzyme"; -import "../../../tests/i18n"; -import EditUserNavLink from "./EditRepositoryRoleNavLink"; - -it("should render nothing, if the edit link is missing", () => { - const user = { - _links: {} - }; - - const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>); - expect(navLink.text()).toBe(""); -}); - -it("should render the navLink", () => { - const user = { - _links: { - update: { - href: "/users" - } - } - }; - - const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>); - expect(navLink.text()).not.toBe(""); -}); diff --git a/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js b/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js deleted file mode 100644 index cc04aa6b50..0000000000 --- a/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.js +++ /dev/null @@ -1,28 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import type { User } from "@scm-manager/ui-types"; -import { NavLink } from "@scm-manager/ui-components"; - -type Props = { - t: string => string, - user: User, - permissionsUrl: String -}; - -class ChangePermissionNavLink extends React.Component<Props> { - render() { - const { t, permissionsUrl } = this.props; - - // if (!this.hasPermissionToSetPermission()) { - // return null; - // } - return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} />; - } - - // hasPermissionToSetPermission = () => { - // return this.props.user._links.permissions; - // }; -} - -export default translate("users")(ChangePermissionNavLink); diff --git a/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js b/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js deleted file mode 100644 index 8cf5231387..0000000000 --- a/scm-ui/src/config/components/navLinks/RepositoryRoleDetailNavLink.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -import { shallow } from "enzyme"; -import "../../../tests/enzyme"; -import "../../../tests/i18n"; -import SetPermissionsNavLink from "./RepositoryRoleDetailNavLink"; - -it("should render nothing, if the permissions link is missing", () => { - const user = { - _links: {} - }; - - const navLink = shallow( - <SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" /> - ); - expect(navLink.text()).toBe(""); -}); - -it("should render the navLink", () => { - const user = { - _links: { - permissions: { - href: "/permissions" - } - } - }; - - const navLink = shallow( - <SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" /> - ); - expect(navLink.text()).not.toBe(""); -}); diff --git a/scm-ui/src/config/components/navLinks/index.js b/scm-ui/src/config/components/navLinks/index.js deleted file mode 100644 index 7fafde676d..0000000000 --- a/scm-ui/src/config/components/navLinks/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as EditRepositoryRoleNavLink } from "./EditRepositoryRoleNavLink"; -export { default as RepositoryRoleDetailNavLink } from "./RepositoryRoleDetailNavLink"; From e62c2084c64f70e719a43166b0457bc1ca06e3f8 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 10:01:33 +0200 Subject: [PATCH 77/91] update RepoRoles Pagination --- scm-ui/src/config/containers/Config.js | 11 ++++++++-- .../roles/containers/EditRepositoryRole.js | 2 +- .../roles/containers/RepositoryRoles.js | 21 +++++++++++++++++++ .../roles/containers/SingleRepositoryRole.js | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 61e5931e24..08ad13b833 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -1,7 +1,7 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import { Route } from "react-router"; +import { Route, Switch } from "react-router-dom"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import type { History } from "history"; import { connect } from "react-redux"; @@ -54,6 +54,7 @@ class Config extends React.Component<Props> { <Page> <div className="columns"> <div className="column is-three-quarters"> + <Switch> <Route path={url} exact component={GlobalConfig} /> <Route path={`${url}/role/:role`} @@ -68,11 +69,17 @@ class Config extends React.Component<Props> { path={`${url}/roles/create`} render={() => <CreateRepositoryRole disabled={false} history={this.props.history} />} /> + <Route + path={`${url}/roles/:page`} + exact + render={() => <RepositoryRoles baseUrl={`${url}/roles/:page`} />} + /> <ExtensionPoint name="config.route" props={extensionProps} renderAll={true} /> + </Switch> </div> <div className="column is-one-quarter"> <Navigation> @@ -82,7 +89,7 @@ class Config extends React.Component<Props> { label={t("config.globalConfigurationNavLink")} /> <NavLink - to={`${url}/roles`} + to={`${url}/roles/`} label={t("repositoryRole.navLink")} activeWhenMatch={this.matchesRoles} /> diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index 98599863fc..0bc7e71e8d 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -25,7 +25,7 @@ class EditRepositoryRole extends React.Component<Props> { repositoryRoleUpdated = (role: Role) => { const { history } = this.props; - history.push("/config/roles"); + history.push("/config/roles/"); }; updateRepositoryRole = (role: Role) => { diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index 35f2888e89..c9c0a5dd49 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -47,6 +47,27 @@ class RepositoryRoles extends React.Component<Props> { fetchRolesByPage(rolesLink, page); } + componentDidUpdate = (prevProps: Props) => { + const { + loading, + list, + page, + rolesLink, + location, + fetchRolesByPage + } = this.props; + if (list && page && !loading) { + const statePage: number = list.page + 1; + if (page !== statePage || prevProps.location.search !== location.search) { + fetchRolesByPage( + rolesLink, + page, + urls.getQueryStringFromLocation(location) + ); + } + } + }; + render() { const { t, loading } = this.props; diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index d42d36e173..e234c7c38b 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -64,7 +64,7 @@ class SingleRepositoryRole extends React.Component<Props> { subtitle={t("repositoryRole.errorSubtitle")} error={error} /> - );0 + ); } if (!role || loading) { From d402b0fe19c16d7287296e2571fbebffe5feda40 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 10:04:57 +0200 Subject: [PATCH 78/91] fix CreateLink after Pagination --- scm-ui/src/config/containers/Config.js | 60 +++++++++++++++----------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 08ad13b833..496dce4611 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -55,30 +55,42 @@ class Config extends React.Component<Props> { <div className="columns"> <div className="column is-three-quarters"> <Switch> - <Route path={url} exact component={GlobalConfig} /> - <Route - path={`${url}/role/:role`} - render={() => <SingleRepositoryRole baseUrl={`${url}/roles`} history={this.props.history} />} - /> - <Route - path={`${url}/roles`} - exact - render={() => <RepositoryRoles baseUrl={`${url}/roles`} />} - /> - <Route - path={`${url}/roles/create`} - render={() => <CreateRepositoryRole disabled={false} history={this.props.history} />} - /> - <Route - path={`${url}/roles/:page`} - exact - render={() => <RepositoryRoles baseUrl={`${url}/roles/:page`} />} - /> - <ExtensionPoint - name="config.route" - props={extensionProps} - renderAll={true} - /> + <Route path={url} exact component={GlobalConfig} /> + <Route + path={`${url}/role/:role`} + render={() => ( + <SingleRepositoryRole + baseUrl={`${url}/roles`} + history={this.props.history} + /> + )} + /> + <Route + path={`${url}/roles`} + exact + render={() => <RepositoryRoles baseUrl={`${url}/roles`} />} + /> + <Route + path={`${url}/roles/create`} + render={() => ( + <CreateRepositoryRole + disabled={false} + history={this.props.history} + /> + )} + /> + <Route + path={`${url}/roles/:page`} + exact + render={() => ( + <RepositoryRoles baseUrl={`${url}/roles`} /> + )} + /> + <ExtensionPoint + name="config.route" + props={extensionProps} + renderAll={true} + /> </Switch> </div> <div className="column is-one-quarter"> From 916de8da8a6b6c5d5c575d43d5eb27efea38cd78 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 10:10:10 +0200 Subject: [PATCH 79/91] add Title to CreateRepositoryRole --- .../src/config/roles/containers/CreateRepositoryRole.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index e77df73deb..1ea0d3f1cf 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -3,6 +3,7 @@ import React from "react"; import RepositoryRoleForm from "./RepositoryRoleForm"; import { connect } from "react-redux"; import { translate } from "react-i18next"; +import { Title } from "@scm-manager/ui-components"; import { createRole, getFetchVerbsFailure, @@ -19,7 +20,10 @@ type Props = { repositoryRolesLink: string, //dispatch function - addRole: (link: string, role: Role, callback?: () => void) => void + addRole: (link: string, role: Role, callback?: () => void) => void, + + // context objects + t: string => string }; class CreateRepositoryRole extends React.Component<Props> { @@ -36,8 +40,10 @@ class CreateRepositoryRole extends React.Component<Props> { }; render() { + const { t } = this.props; return ( <> + <Title title={t("repositoryRole.title")} /> <RepositoryRoleForm disabled={this.props.disabled} submitForm={role => this.createRepositoryRole(role)} From 3461f6680b6280dc21dec4eb6655c7fe2a13320d Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 10:50:12 +0200 Subject: [PATCH 80/91] show verb translations --- .../config/roles/components/AvailableVerbs.js | 46 +++++++++++++++++++ .../components/PermissionRoleDetailsTable.js | 40 +++------------- 2 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 scm-ui/src/config/roles/components/AvailableVerbs.js diff --git a/scm-ui/src/config/roles/components/AvailableVerbs.js b/scm-ui/src/config/roles/components/AvailableVerbs.js new file mode 100644 index 0000000000..1b27f95b83 --- /dev/null +++ b/scm-ui/src/config/roles/components/AvailableVerbs.js @@ -0,0 +1,46 @@ +//@flow +import React from "react"; +import type { Role } from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import { compose } from "redux"; +import injectSheet from "react-jss"; + +type Props = { + role: Role, + // context props + t: string => string +}; + +const styles = { + spacing: { + padding: "0 !important" + } +}; + +class AvailableVerbs extends React.Component<Props> { + + render() { + const { role, t, classes } = this.props; + + let verbs = null; + if (role.verbs.length > 0) { + verbs = ( + <tr> + <td className={classes.spacing}> + <ul> + {role.verbs.map(verb => { + return <li>{t("verbs.repository." + verb + ".displayName")}</li>; + })} + </ul> + </td> + </tr> + ); + } + return (verbs); + } +} + +export default compose( + injectSheet(styles), + translate("plugins") +)(AvailableVerbs); diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js index 67d752fb8c..f320547237 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js @@ -2,8 +2,7 @@ import React from "react"; import type { Role } from "@scm-manager/ui-types"; import { translate } from "react-i18next"; -import { compose } from "redux"; -import injectSheet from "react-jss"; +import AvailableVerbs from "./AvailableVerbs"; type Props = { role: Role, @@ -11,12 +10,6 @@ type Props = { t: string => string }; -const styles = { - spacing: { - padding: "0 !important" - } -}; - class PermissionRoleDetailsTable extends React.Component<Props> { render() { const { role, t } = this.props; @@ -31,35 +24,14 @@ class PermissionRoleDetailsTable extends React.Component<Props> { <th>{t("repositoryRole.type")}</th> <td>{role.type}</td> </tr> - {this.renderVerbs()} + <tr> + <th>{t("repositoryRole.verbs")}</th> + <AvailableVerbs role={role} /> + </tr> </tbody> </table> ); } - - renderVerbs() { - const { role, t, classes } = this.props; - - let verbs = null; - if (role.verbs.length > 0) { - verbs = ( - <tr> - <th>{t("repositoryRole.verbs")}</th> - <td className={classes.spacing}> - <ul> - {role.verbs.map(verb => { - return <li>{verb}</li>; - })} - </ul> - </td> - </tr> - ); - } - return verbs; - } } -export default compose( - injectSheet(styles), - translate("config") -)(PermissionRoleDetailsTable); +export default translate("config")(PermissionRoleDetailsTable); From e34781789dc259c68ac6f18dfaff5f5aca663488 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer <rene.pfeuffer@cloudogu.com> Date: Thu, 16 May 2019 12:12:46 +0200 Subject: [PATCH 81/91] Set role instead of collection of verbs for repository owner --- .../scm/api/v2/resources/RepositoryCollectionResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index b4d148b4bf..8b351aa46a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -106,7 +106,7 @@ public class RepositoryCollectionResource { private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) { Repository repository = dtoToRepositoryMapper.map(repositoryDto, null); - repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), singletonList("*"), false))); + repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), "OWNER", false))); return repository; } From 46456d0390e776fe83142fd3dc46ad8797df5da4 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 16 May 2019 12:17:18 +0200 Subject: [PATCH 82/91] add Errorhandling to CreateRepoRole & EditRepoRole --- .../roles/containers/CreateRepositoryRole.js | 14 +++++++---- .../roles/containers/EditRepositoryRole.js | 23 +++++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index 1ea0d3f1cf..4a572a3206 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -3,9 +3,10 @@ import React from "react"; import RepositoryRoleForm from "./RepositoryRoleForm"; import { connect } from "react-redux"; import { translate } from "react-i18next"; -import { Title } from "@scm-manager/ui-components"; +import { ErrorNotification, Title } from "@scm-manager/ui-components"; import { createRole, + getCreateRoleFailure, getFetchVerbsFailure, isFetchVerbsPending } from "../modules/roles"; @@ -18,6 +19,7 @@ import { type Props = { disabled: boolean, repositoryRolesLink: string, + error?: Error, //dispatch function addRole: (link: string, role: Role, callback?: () => void) => void, @@ -27,7 +29,6 @@ type Props = { }; class CreateRepositoryRole extends React.Component<Props> { - repositoryRoleCreated = (role: Role) => { const { history } = this.props; history.push("/config/role/" + role.name + "/info"); @@ -40,7 +41,12 @@ class CreateRepositoryRole extends React.Component<Props> { }; render() { - const { t } = this.props; + const { t, error } = this.props; + + if (error) { + return <ErrorNotification error={error} />; + } + return ( <> <Title title={t("repositoryRole.title")} /> @@ -55,7 +61,7 @@ class CreateRepositoryRole extends React.Component<Props> { const mapStateToProps = (state, ownProps) => { const loading = isFetchVerbsPending(state); - const error = getFetchVerbsFailure(state); + const error = getFetchVerbsFailure(state) || getCreateRoleFailure(state); const verbsLink = getRepositoryVerbsLink(state); const repositoryRolesLink = getRepositoryRolesLink(state); diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index 0bc7e71e8d..1555c63ce8 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -6,35 +6,38 @@ import { translate } from "react-i18next"; import { getModifyRoleFailure, isModifyRolePending, - modifyRole, + modifyRole } from "../modules/roles"; +import { ErrorNotification } from "@scm-manager/ui-components"; import type { Role } from "@scm-manager/ui-types"; type Props = { disabled: boolean, role: Role, repositoryRolesLink: string, + error?: Error, //dispatch function updateRole: (link: string, role: Role, callback?: () => void) => void }; - - class EditRepositoryRole extends React.Component<Props> { - repositoryRoleUpdated = (role: Role) => { const { history } = this.props; history.push("/config/roles/"); }; updateRepositoryRole = (role: Role) => { - this.props.updateRole(role, () => - this.repositoryRoleUpdated(role) - ); + this.props.updateRole(role, () => this.repositoryRoleUpdated(role)); }; render() { + const { error } = this.props; + + if (error) { + return <ErrorNotification error={error} />; + } + return ( <> <RepositoryRoleForm @@ -44,16 +47,16 @@ class EditRepositoryRole extends React.Component<Props> { /> </> ); - }w + } } const mapStateToProps = (state, ownProps) => { const loading = isModifyRolePending(state); - const error = getModifyRoleFailure(state); + const error = getModifyRoleFailure(state, ownProps.role.name); return { loading, - error, + error }; }; From bb2dc3738d2148afb87d98cd3d784c194341b30c Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer <rene.pfeuffer@cloudogu.com> Date: Thu, 16 May 2019 12:19:52 +0200 Subject: [PATCH 83/91] Take verbs of role for initial advanced permission settings --- .../permissions/containers/CreatePermissionForm.js | 5 ++++- .../permissions/containers/SinglePermission.js | 13 +++++++++---- .../src/repos/permissions/modules/permissions.js | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 8ee1e7edbb..c09baf50fa 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -17,6 +17,7 @@ import { import * as validator from "../components/permissionValidation"; import RoleSelector from "../components/RoleSelector"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; +import { findVerbsForRole } from "../modules/permissions"; type Props = { availableRoles: RepositoryRole[], @@ -142,10 +143,12 @@ class CreatePermissionForm extends React.Component<Props, State> { const availableRoleNames = availableRoles.map(r => r.name); + const selectedVerbs = role ? findVerbsForRole(availableRoles, role) : verbs; + const advancedDialog = showAdvancedDialog ? ( <AdvancedPermissionsDialog availableVerbs={availableVerbs} - selectedVerbs={verbs} + selectedVerbs={selectedVerbs} onClose={this.closeAdvancedPermissionsDialog} onSubmit={this.submitAdvancedPermissionsDialog} /> diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js index 5fed0084a7..bdb96de0bd 100644 --- a/scm-ui/src/repos/permissions/containers/SinglePermission.js +++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js @@ -6,7 +6,8 @@ import { modifyPermission, isModifyPermissionPending, deletePermission, - isDeletePermissionPending + isDeletePermissionPending, + findVerbsForRole } from "../modules/permissions"; import { connect } from "react-redux"; import type { History } from "history"; @@ -131,11 +132,15 @@ class SinglePermission extends React.Component<Props, State> { </td> ); - const advancedDialg = showAdvancedDialog ? ( + const selectedVerbs = permission.role + ? findVerbsForRole(availableRepositoryRoles, permission.role) + : permission.verbs; + + const advancedDialog = showAdvancedDialog ? ( <AdvancedPermissionsDialog readOnly={readOnly} availableVerbs={availableRepositoryVerbs} - selectedVerbs={permission.verbs} + selectedVerbs={selectedVerbs} onClose={this.closeAdvancedPermissionsDialog} onSubmit={this.submitAdvancedPermissionsDialog} /> @@ -174,7 +179,7 @@ class SinglePermission extends React.Component<Props, State> { deletePermission={this.deletePermission} loading={this.props.deleteLoading} /> - {advancedDialg} + {advancedDialog} </td> </tr> ); diff --git a/scm-ui/src/repos/permissions/modules/permissions.js b/scm-ui/src/repos/permissions/modules/permissions.js index abd25eb459..276f28f672 100644 --- a/scm-ui/src/repos/permissions/modules/permissions.js +++ b/scm-ui/src/repos/permissions/modules/permissions.js @@ -739,3 +739,17 @@ export function getModifyPermissionsFailure( } return null; } + +export function findVerbsForRole( + availableRepositoryRoles: RepositoryRole[], + roleName: string +) { + const matchingRole = availableRepositoryRoles.find( + role => roleName === role.name + ); + if (matchingRole) { + return matchingRole.verbs; + } else { + return []; + } +} From 510ea51e769eda1fa907ab5053e51dd7998c64e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Thu, 16 May 2019 13:06:08 +0200 Subject: [PATCH 84/91] Fix unit test --- .../sonia/scm/api/v2/resources/RepositoryRootResourceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 55754469ec..c47250470d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -332,7 +332,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .hasSize(1) .allSatisfy(p -> { assertThat(p.getName()).isEqualTo("trillian"); - assertThat(p.getVerbs()).containsExactly("*"); + assertThat(p.getRole()).isEqualTo("OWNER"); }); } From 3ea48103463766aadeaec84ba5dd66073c9e483b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Thu, 16 May 2019 13:36:11 +0200 Subject: [PATCH 85/91] Finish rename Role to RepositoryRole --- .../packages/ui-types/src/RepositoryRole.js | 3 ++ .../packages/ui-types/src/Role.js | 12 ----- .../packages/ui-types/src/index.js | 1 - .../config/roles/components/AvailableVerbs.js | 11 ++--- .../roles/components/PermissionRoleDetails.js | 4 +- .../components/PermissionRoleDetailsTable.js | 4 +- .../roles/components/PermissionRoleRow.js | 4 +- .../roles/components/PermissionRoleTable.js | 4 +- .../roles/containers/CreateRepositoryRole.js | 10 ++--- .../roles/containers/EditRepositoryRole.js | 16 ++++--- .../roles/containers/RepositoryRoleForm.js | 21 +++++---- .../roles/containers/RepositoryRoles.js | 29 +++++++----- .../roles/containers/SingleRepositoryRole.js | 36 +++++++-------- scm-ui/src/config/roles/modules/roles.js | 45 ++++++++++--------- 14 files changed, 105 insertions(+), 95 deletions(-) delete mode 100644 scm-ui-components/packages/ui-types/src/Role.js diff --git a/scm-ui-components/packages/ui-types/src/RepositoryRole.js b/scm-ui-components/packages/ui-types/src/RepositoryRole.js index 8b8ffc933f..195bdfe05c 100644 --- a/scm-ui-components/packages/ui-types/src/RepositoryRole.js +++ b/scm-ui-components/packages/ui-types/src/RepositoryRole.js @@ -1,10 +1,13 @@ // @flow +import type {Links} from "./hal"; + export type RepositoryRole = { name: string, verbs: string[], type?: string, creationDate?: string, lastModified?: string, + _links: Links }; diff --git a/scm-ui-components/packages/ui-types/src/Role.js b/scm-ui-components/packages/ui-types/src/Role.js deleted file mode 100644 index 00e794b886..0000000000 --- a/scm-ui-components/packages/ui-types/src/Role.js +++ /dev/null @@ -1,12 +0,0 @@ -//@flow - -import type { Links } from "./hal"; - -export type Role = { - name: string, - verbs: string[], - creationDate?: number, - lastModified?: number, - system: boolean, - _links: Links -}; diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index 0272b1e2cb..4024710300 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -16,7 +16,6 @@ export type { Changeset } from "./Changesets"; export type { Tag } from "./Tags"; export type { Config } from "./Config"; -export type { Role } from "./Role"; export type { IndexResources } from "./IndexResources"; diff --git a/scm-ui/src/config/roles/components/AvailableVerbs.js b/scm-ui/src/config/roles/components/AvailableVerbs.js index 1b27f95b83..dbe0b50a77 100644 --- a/scm-ui/src/config/roles/components/AvailableVerbs.js +++ b/scm-ui/src/config/roles/components/AvailableVerbs.js @@ -1,12 +1,12 @@ //@flow import React from "react"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import { translate } from "react-i18next"; import { compose } from "redux"; import injectSheet from "react-jss"; type Props = { - role: Role, + role: RepositoryRole, // context props t: string => string }; @@ -18,7 +18,6 @@ const styles = { }; class AvailableVerbs extends React.Component<Props> { - render() { const { role, t, classes } = this.props; @@ -29,14 +28,16 @@ class AvailableVerbs extends React.Component<Props> { <td className={classes.spacing}> <ul> {role.verbs.map(verb => { - return <li>{t("verbs.repository." + verb + ".displayName")}</li>; + return ( + <li>{t("verbs.repository." + verb + ".displayName")}</li> + ); })} </ul> </td> </tr> ); } - return (verbs); + return verbs; } } diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index fa68a66d25..1977ddde2a 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -1,13 +1,13 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint"; import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable"; import { Button, Subtitle } from "@scm-manager/ui-components"; type Props = { - role: Role, + role: RepositoryRole, url: string, // context props diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js index f320547237..dd0502cb02 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js @@ -1,11 +1,11 @@ //@flow import React from "react"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import { translate } from "react-i18next"; import AvailableVerbs from "./AvailableVerbs"; type Props = { - role: Role, + role: RepositoryRole, // context props t: string => string }; diff --git a/scm-ui/src/config/roles/components/PermissionRoleRow.js b/scm-ui/src/config/roles/components/PermissionRoleRow.js index 9f06eb56de..8a10bf93d0 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleRow.js +++ b/scm-ui/src/config/roles/components/PermissionRoleRow.js @@ -1,12 +1,12 @@ // @flow import React from "react"; import { Link } from "react-router-dom"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import SystemRoleTag from "./SystemRoleTag"; type Props = { baseUrl: string, - role: Role + role: RepositoryRole }; class PermissionRoleRow extends React.Component<Props> { diff --git a/scm-ui/src/config/roles/components/PermissionRoleTable.js b/scm-ui/src/config/roles/components/PermissionRoleTable.js index a585a84bca..bb98f7c3e9 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleTable.js @@ -1,12 +1,12 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import PermissionRoleRow from "./PermissionRoleRow"; type Props = { baseUrl: string, - roles: Role[], + roles: RepositoryRole[], t: string => string }; diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index 4a572a3206..88f42f8353 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -10,7 +10,7 @@ import { getFetchVerbsFailure, isFetchVerbsPending } from "../modules/roles"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import { getRepositoryRolesLink, getRepositoryVerbsLink @@ -22,19 +22,19 @@ type Props = { error?: Error, //dispatch function - addRole: (link: string, role: Role, callback?: () => void) => void, + addRole: (link: string, role: RepositoryRole, callback?: () => void) => void, // context objects t: string => string }; class CreateRepositoryRole extends React.Component<Props> { - repositoryRoleCreated = (role: Role) => { + repositoryRoleCreated = (role: RepositoryRole) => { const { history } = this.props; history.push("/config/role/" + role.name + "/info"); }; - createRepositoryRole = (role: Role) => { + createRepositoryRole = (role: RepositoryRole) => { this.props.addRole(this.props.repositoryRolesLink, role, () => this.repositoryRoleCreated(role) ); @@ -75,7 +75,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - addRole: (link: string, role: Role, callback?: () => void) => { + addRole: (link: string, role: RepositoryRole, callback?: () => void) => { dispatch(createRole(link, role, callback)); } }; diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index 1555c63ce8..deb0a77583 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -9,25 +9,29 @@ import { modifyRole } from "../modules/roles"; import { ErrorNotification } from "@scm-manager/ui-components"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; type Props = { disabled: boolean, - role: Role, + role: RepositoryRole, repositoryRolesLink: string, error?: Error, //dispatch function - updateRole: (link: string, role: Role, callback?: () => void) => void + updateRole: ( + link: string, + role: RepositoryRole, + callback?: () => void + ) => void }; class EditRepositoryRole extends React.Component<Props> { - repositoryRoleUpdated = (role: Role) => { + repositoryRoleUpdated = (role: RepositoryRole) => { const { history } = this.props; history.push("/config/roles/"); }; - updateRepositoryRole = (role: Role) => { + updateRepositoryRole = (role: RepositoryRole) => { this.props.updateRole(role, () => this.repositoryRoleUpdated(role)); }; @@ -62,7 +66,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - updateRole: (role: Role, callback?: () => void) => { + updateRole: (role: RepositoryRole, callback?: () => void) => { dispatch(modifyRole(role, callback)); } }; diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index 3515cddcfb..43d901a285 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -2,7 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import { InputField, SubmitButton } from "@scm-manager/ui-components"; import PermissionCheckbox from "../../../repos/permissions/components/PermissionCheckbox"; import { @@ -11,15 +11,18 @@ import { getVerbsFromState, isFetchVerbsPending } from "../modules/roles"; -import {getRepositoryRolesLink, getRepositoryVerbsLink} from "../../../modules/indexResource"; +import { + getRepositoryRolesLink, + getRepositoryVerbsLink +} from "../../../modules/indexResource"; type Props = { - role?: Role, + role?: RepositoryRole, loading?: boolean, nameDisabled: boolean, availableVerbs: string[], verbsLink: string, - submitForm: Role => void, + submitForm: RepositoryRole => void, // context objects t: string => string, @@ -29,7 +32,7 @@ type Props = { }; type State = { - role: Role + role: RepositoryRole }; class RepositoryRoleForm extends React.Component<Props, State> { @@ -47,10 +50,10 @@ class RepositoryRoleForm extends React.Component<Props, State> { } componentDidMount() { - const { fetchAvailableVerbs, verbsLink} = this.props; + const { fetchAvailableVerbs, verbsLink } = this.props; fetchAvailableVerbs(verbsLink); if (this.props.role) { - this.setState({role: this.props.role}) + this.setState({ role: this.props.role }); } } @@ -95,7 +98,7 @@ class RepositoryRoleForm extends React.Component<Props, State> { submit = (event: Event) => { event.preventDefault(); if (this.isValid()) { - this.props.submitForm(this.state.role) + this.props.submitForm(this.state.role); } }; @@ -163,7 +166,7 @@ const mapDispatchToProps = dispatch => { return { fetchAvailableVerbs: (link: string) => { dispatch(fetchAvailableVerbs(link)); - }, + } }; }; diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index c9c0a5dd49..f148b120e0 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -1,10 +1,10 @@ // @flow import React from "react"; import { connect } from "react-redux"; -import {withRouter} from "react-router-dom"; +import { withRouter } from "react-router-dom"; import { translate } from "react-i18next"; import type { History } from "history"; -import type { Role, PagedCollection } from "@scm-manager/ui-types"; +import type { RepositoryRole, PagedCollection } from "@scm-manager/ui-types"; import { Title, Loading, @@ -25,7 +25,7 @@ import PermissionRoleTable from "../components/PermissionRoleTable"; import { getRolesLink } from "../../../modules/indexResource"; type Props = { baseUrl: string, - roles: Role[], + roles: RepositoryRole[], loading: boolean, error: Error, canAddRoles: boolean, @@ -90,19 +90,26 @@ class RepositoryRoles extends React.Component<Props> { return ( <> <PermissionRoleTable baseUrl={baseUrl} roles={roles} /> - <LinkPaginator collection={list} page={page} /> + <LinkPaginator collection={list} page={page} /> </> ); } return ( - <Notification type="info">{t("repositoryRole.noPermissionRoles")}</Notification> + <Notification type="info"> + {t("repositoryRole.noPermissionRoles")} + </Notification> ); } renderCreateButton() { const { canAddRoles, baseUrl, t } = this.props; if (canAddRoles) { - return <CreateButton label={t("repositoryRole.createButton")} link={`${baseUrl}/create`} />; + return ( + <CreateButton + label={t("repositoryRole.createButton")} + link={`${baseUrl}/create`} + /> + ); } return null; } @@ -137,7 +144,9 @@ const mapDispatchToProps = dispatch => { }; }; -export default withRouter(connect( - mapStateToProps, - mapDispatchToProps -)(translate("config")(RepositoryRoles))); +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(translate("config")(RepositoryRoles)) +); diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index e234c7c38b..3bb951ac1a 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -5,7 +5,7 @@ import { Loading, ErrorPage, Title } from "@scm-manager/ui-components"; import { Route } from "react-router"; import type { History } from "history"; import { translate } from "react-i18next"; -import type { Role } from "@scm-manager/ui-types"; +import type { RepositoryRole } from "@scm-manager/ui-types"; import { getRepositoryRolesLink } from "../../../modules/indexResource"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { @@ -20,7 +20,7 @@ import EditRepositoryRole from "./EditRepositoryRole"; type Props = { roleName: string, - role: Role, + role: RepositoryRole, loading: boolean, error: Error, repositoryRolesLink: string, @@ -83,22 +83,22 @@ class SingleRepositoryRole extends React.Component<Props> { <Title title={t("repositoryRole.title")} /> <div className="columns"> <div className="column is-three-quarters"> - <Route - path={`${url}/info`} - component={() => ( - <PermissionRoleDetail role={role} url={url} /> - )} - /> - <Route - path={`${url}/edit`} - exact - component={() => <EditRepositoryRole role={role} history={this.props.history} />} - /> - <ExtensionPoint - name="roles.route" - props={extensionProps} - renderAll={true} - /> + <Route + path={`${url}/info`} + component={() => <PermissionRoleDetail role={role} url={url} />} + /> + <Route + path={`${url}/edit`} + exact + component={() => ( + <EditRepositoryRole role={role} history={this.props.history} /> + )} + /> + <ExtensionPoint + name="roles.route" + props={extensionProps} + renderAll={true} + /> </div> </div> </> diff --git a/scm-ui/src/config/roles/modules/roles.js b/scm-ui/src/config/roles/modules/roles.js index a7a3ce3d93..fffc395e9e 100644 --- a/scm-ui/src/config/roles/modules/roles.js +++ b/scm-ui/src/config/roles/modules/roles.js @@ -4,7 +4,11 @@ import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; import * as types from "../../../modules/types"; import { combineReducers, Dispatch } from "redux"; -import type {Action, PagedCollection, Repository, Role} from "@scm-manager/ui-types"; +import type { + Action, + PagedCollection, + RepositoryRole +} from "@scm-manager/ui-types"; export const FETCH_ROLES = "scm/roles/FETCH_ROLES"; export const FETCH_ROLES_PENDING = `${FETCH_ROLES}_${types.PENDING_SUFFIX}`; @@ -138,12 +142,12 @@ export function fetchRoleByName(link: string, name: string) { return fetchRole(roleUrl, name); } -export function fetchRoleByLink(role: Role) { +export function fetchRoleByLink(role: RepositoryRole) { return fetchRole(role._links.self.href, role.name); } // create role -export function createRolePending(role: Role): Action { +export function createRolePending(role: RepositoryRole): Action { return { type: CREATE_ROLE_PENDING, role @@ -169,7 +173,11 @@ export function createRoleReset() { }; } -export function createRole(link: string, role: Role, callback?: () => void) { +export function createRole( + link: string, + role: RepositoryRole, + callback?: () => void +) { return function(dispatch: Dispatch) { dispatch(createRolePending(role)); return apiClient @@ -233,7 +241,7 @@ function verbReducer(state: any = {}, action: any = {}) { } // modify role -export function modifyRolePending(role: Role): Action { +export function modifyRolePending(role: RepositoryRole): Action { return { type: MODIFY_ROLE_PENDING, payload: role, @@ -241,7 +249,7 @@ export function modifyRolePending(role: Role): Action { }; } -export function modifyRoleSuccess(role: Role): Action { +export function modifyRoleSuccess(role: RepositoryRole): Action { return { type: MODIFY_ROLE_SUCCESS, payload: role, @@ -249,7 +257,7 @@ export function modifyRoleSuccess(role: Role): Action { }; } -export function modifyRoleFailure(role: Role, error: Error): Action { +export function modifyRoleFailure(role: RepositoryRole, error: Error): Action { return { type: MODIFY_ROLE_FAILURE, payload: { @@ -260,14 +268,14 @@ export function modifyRoleFailure(role: Role, error: Error): Action { }; } -export function modifyRoleReset(role: Role): Action { +export function modifyRoleReset(role: RepositoryRole): Action { return { type: MODIFY_ROLE_RESET, itemId: role.name }; } -export function modifyRole(role: Role, callback?: () => void) { +export function modifyRole(role: RepositoryRole, callback?: () => void) { return function(dispatch: Dispatch) { dispatch(modifyRolePending(role)); return apiClient @@ -288,7 +296,7 @@ export function modifyRole(role: Role, callback?: () => void) { } // delete role -export function deleteRolePending(role: Role): Action { +export function deleteRolePending(role: RepositoryRole): Action { return { type: DELETE_ROLE_PENDING, payload: role, @@ -296,7 +304,7 @@ export function deleteRolePending(role: Role): Action { }; } -export function deleteRoleSuccess(role: Role): Action { +export function deleteRoleSuccess(role: RepositoryRole): Action { return { type: DELETE_ROLE_SUCCESS, payload: role, @@ -304,7 +312,7 @@ export function deleteRoleSuccess(role: Role): Action { }; } -export function deleteRoleFailure(role: Role, error: Error): Action { +export function deleteRoleFailure(role: RepositoryRole, error: Error): Action { return { type: DELETE_ROLE_FAILURE, payload: { @@ -315,7 +323,7 @@ export function deleteRoleFailure(role: Role, error: Error): Action { }; } -export function deleteRole(role: Role, callback?: () => void) { +export function deleteRole(role: RepositoryRole, callback?: () => void) { return function(dispatch: any) { dispatch(deleteRolePending(role)); return apiClient @@ -333,7 +341,7 @@ export function deleteRole(role: Role, callback?: () => void) { } function extractRolesByNames( - roles: Role[], + roles: RepositoryRole[], roleNames: string[], oldRolesByNames: Object ) { @@ -460,7 +468,7 @@ export function getRolesFromState(state: Object) { if (!roleNames) { return null; } - const roleEntries: Role[] = []; + const roleEntries: RepositoryRole[] = []; for (let roleName of roleNames) { roleEntries.push(state.roles.byNames[roleName]); @@ -470,12 +478,7 @@ export function getRolesFromState(state: Object) { } export function getRoleCreateLink(state: Object) { - if ( - state && - state.list && - state.list._links && - state.list._links.create - ) { + if (state && state.list && state.list._links && state.list._links.create) { return state.list._links.create.href; } } From 781198d9a7b65204ce18ec2bf391145265e0c35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Thu, 16 May 2019 13:42:41 +0200 Subject: [PATCH 86/91] Reset verbs when a role is selected --- .../src/repos/permissions/containers/CreatePermissionForm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index c09baf50fa..765d752d37 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -265,7 +265,8 @@ class CreatePermissionForm extends React.Component<Props, State> { return; } this.setState({ - role: selectedRole.name + role: selectedRole.name, + verbs: [] }); }; From 4a08b47bd105d97a2700d5865d3c54e723dd52c9 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer <rene.pfeuffer@cloudogu.com> Date: Thu, 16 May 2019 11:53:42 +0000 Subject: [PATCH 87/91] Close branch feature/custom_roles_overview From a52cd625b87fb7250d4b4b5c67a29cc956ab0a92 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 17 May 2019 08:50:32 +0200 Subject: [PATCH 88/91] add DeleteRepositoryRole --- scm-ui/public/locales/en/config.json | 10 ++ .../roles/containers/DeleteRepositoryRole.js | 113 ++++++++++++++++++ .../roles/containers/EditRepositoryRole.js | 3 + 3 files changed, 126 insertions(+) create mode 100644 scm-ui/src/config/roles/containers/DeleteRepositoryRole.js diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index bdb245544a..44da3c1be2 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -33,6 +33,16 @@ "name": "Name", "system": "System" }, + "deleteRole" : { + "button": "Delete", + "subtitle": "Delete Permission Role", + "confirmAlert": { + "title": "Delete Permission Role", + "message": "Do you really want to delete this permission role? All users who own this role will lose their permissions.", + "submit": "Yes", + "cancel": "No" + } + }, "config-form": { "submit": "Submit", "submit-success-notification": "Configuration changed successfully!", diff --git a/scm-ui/src/config/roles/containers/DeleteRepositoryRole.js b/scm-ui/src/config/roles/containers/DeleteRepositoryRole.js new file mode 100644 index 0000000000..a00cc21840 --- /dev/null +++ b/scm-ui/src/config/roles/containers/DeleteRepositoryRole.js @@ -0,0 +1,113 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { RepositoryRole } from "@scm-manager/ui-types"; +import { + Subtitle, + DeleteButton, + confirmAlert, + ErrorNotification +} from "@scm-manager/ui-components"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; +import { + deleteRole, + getDeleteRoleFailure, + isDeleteRolePending +} from "../modules/roles"; + +type Props = { + loading: boolean, + error: Error, + role: RepositoryRole, + confirmDialog?: boolean, + deleteRole: (role: RepositoryRole, callback?: () => void) => void, + + // context props + history: History, + t: string => string +}; + +class DeleteRepositoryRole extends React.Component<Props> { + static defaultProps = { + confirmDialog: true + }; + + roleDeleted = () => { + this.props.history.push("/config/roles/"); + }; + + deleteRole = () => { + this.props.deleteRole(this.props.role, this.roleDeleted); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("deleteRole.confirmAlert.title"), + message: t("deleteRole.confirmAlert.message"), + buttons: [ + { + label: t("deleteRole.confirmAlert.submit"), + onClick: () => this.deleteRole() + }, + { + label: t("deleteRole.confirmAlert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.role._links.delete; + }; + + render() { + const { loading, error, confirmDialog, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deleteRole; + + if (!this.isDeletable()) { + return null; + } + + return ( + <> + <Subtitle subtitle={t("deleteRole.subtitle")} /> + <div className="columns"> + <div className="column"> + <ErrorNotification error={error} /> + <DeleteButton + label={t("deleteRole.button")} + action={action} + loading={loading} + /> + </div> + </div> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const loading = isDeleteRolePending(state, ownProps.role.name); + const error = getDeleteRoleFailure(state, ownProps.role.name); + return { + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteRole: (role: RepositoryRole, callback?: () => void) => { + dispatch(deleteRole(role, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(translate("config")(DeleteRepositoryRole))); diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index deb0a77583..f63a2f3e50 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -10,6 +10,7 @@ import { } from "../modules/roles"; import { ErrorNotification } from "@scm-manager/ui-components"; import type { RepositoryRole } from "@scm-manager/ui-types"; +import DeleteRepositoryRole from "./DeleteRepositoryRole"; type Props = { disabled: boolean, @@ -49,6 +50,8 @@ class EditRepositoryRole extends React.Component<Props> { role={this.props.role} submitForm={role => this.updateRepositoryRole(role)} /> + <hr/> + <DeleteRepositoryRole role={this.props.role}/> </> ); } From f5ded2f28da9d48bc501b430a76ebda2b3d51b53 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 17 May 2019 09:03:49 +0200 Subject: [PATCH 89/91] add German Translations for DeleteRepositoryRole --- scm-ui/public/locales/de/config.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index 71ab66ff22..341fc978a5 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -33,6 +33,16 @@ "name": "Name", "system": "System" }, + "deleteRole" : { + "button": "Löschen", + "subtitle": "Berechtigungsrolle löschen", + "confirmAlert": { + "title": "Berechtigungsrolle löschen", + "message": "Soll die Berechtigungsrolle wirklich gelöscht werden? Alle Nutzer dieser Rolle verlieren Ihre Berechtigungen.", + "submit": "Ja", + "cancel": "Nein" + } + }, "config-form": { "submit": "Speichern", "submit-success-notification": "Einstellungen wurden erfolgreich geändert!", From c60ecea807597b54aec10d5cccdc78ff7951b4bd Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 17 May 2019 11:24:37 +0200 Subject: [PATCH 90/91] rename verbs to permissions --- scm-ui/public/locales/de/config.json | 2 +- scm-ui/public/locales/en/config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index 341fc978a5..d26552c30d 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -14,7 +14,7 @@ "createButton": "Berechtigungsrolle erstellen", "name": "Name", "type": "Typ", - "verbs": "Verben", + "verbs": "Berechtigungen", "button": { "edit": "Bearbeiten" }, diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 44da3c1be2..b2f6e77e0d 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -14,7 +14,7 @@ "createButton": "Create Permission Role", "name": "Name", "type": "Type", - "verbs": "Verbs", + "verbs": "Permissions", "edit": "Edit Permission Role", "button": { "edit": "Edit" From 406fdac7095d161d61f9fee74b87adf94a3cb2fc Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer <rene.pfeuffer@cloudogu.com> Date: Fri, 17 May 2019 13:56:14 +0000 Subject: [PATCH 91/91] Close branch feature/custom_roles_overview