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 001/118] 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 002/118] 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 003/118] 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 004/118] 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 005/118] 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 006/118] 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 007/118] 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 008/118] 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 009/118] 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 010/118] 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 011/118] 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 012/118] 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 6d325f56e17605477449527f496d5b9e5b4eac41 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 7 May 2019 10:11:26 +0200 Subject: [PATCH 013/118] adds option to render markdown headings with anchor links --- .../src/MarkdownHeadingRenderer.js | 35 +++++++++++++++++++ .../src/MarkdownHeadingRenderer.test.js | 18 ++++++++++ .../ui-components/src/MarkdownView.js | 16 ++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js create mode 100644 scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js new file mode 100644 index 0000000000..d7268c2861 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js @@ -0,0 +1,35 @@ +// @flow +import * as React from "react"; + +/** + * Adds anchor links to markdown headings. + * + * @see Headings are missing anchors / ids + */ + +type Props = { + children: React.Node, + level: number +}; + +function flatten(text: string, child: any) { + return typeof child === "string" + ? text + child + : React.Children.toArray(child.props.children).reduce(flatten, text); +} + +/** + * Turns heading text into a anchor id + * + * @VisibleForTesting + */ +export function headingToAnchorId(heading: string) { + return heading.toLowerCase().replace(/\W/g, "-"); +} + +export default function MarkdownHeadingRenderer(props: Props) { + const children = React.Children.toArray(props.children); + const heading = children.reduce(flatten, ""); + const anchorId = headingToAnchorId(heading); + return React.createElement("h" + props.level, {id: anchorId}, props.children); +} diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js new file mode 100644 index 0000000000..4fd8428e98 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js @@ -0,0 +1,18 @@ +// @flow +import React from "react"; +import { headingToAnchorId } from "./MarkdownHeadingRenderer"; + +describe("headingToAnchorId tests", () => { + + it("should lower case the text", () => { + expect(headingToAnchorId("Hello")).toBe("hello"); + expect(headingToAnchorId("HeLlO")).toBe("hello"); + expect(headingToAnchorId("HELLO")).toBe("hello"); + }); + + it("should replace spaces with hyphen", () => { + expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff"); + expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f"); + }); + +}); diff --git a/scm-ui-components/packages/ui-components/src/MarkdownView.js b/scm-ui-components/packages/ui-components/src/MarkdownView.js index 4d2b2de92f..164c06b84a 100644 --- a/scm-ui-components/packages/ui-components/src/MarkdownView.js +++ b/scm-ui-components/packages/ui-components/src/MarkdownView.js @@ -3,17 +3,27 @@ import React from "react"; import SyntaxHighlighter from "./SyntaxHighlighter"; import Markdown from "react-markdown/with-html"; import {binder} from "@scm-manager/ui-extensions"; +import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; type Props = { content: string, renderContext?: Object, renderers?: Object, + enableAnchorHeadings: boolean }; class MarkdownView extends React.Component { + static defaultProps = { + enableAnchorHeadings: false + }; + + constructor(props: Props) { + super(props); + } + render() { - const {content, renderers, renderContext} = this.props; + const {content, renderers, renderContext, enableAnchorHeadings} = this.props; const rendererFactory = binder.getExtension("markdown-renderer-factory"); let rendererList = renderers; @@ -26,6 +36,10 @@ class MarkdownView extends React.Component { rendererList = {}; } + if (enableAnchorHeadings) { + rendererList.heading = MarkdownHeadingRenderer; + } + if (!rendererList.code){ rendererList.code = SyntaxHighlighter; } 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 014/118] 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 f99e685eea459857acc7a02b8f9b6a81281208df Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 7 May 2019 16:41:28 +0200 Subject: [PATCH 015/118] fix scroll to anchor link on page reload --- scm-ui/src/containers/ScrollToTop.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/containers/ScrollToTop.js b/scm-ui/src/containers/ScrollToTop.js index d48ea6531a..77054b8463 100644 --- a/scm-ui/src/containers/ScrollToTop.js +++ b/scm-ui/src/containers/ScrollToTop.js @@ -11,7 +11,15 @@ type Props = { class ScrollToTop extends React.Component { componentDidUpdate(prevProps) { if (this.props.location.pathname !== prevProps.location.pathname) { - window.scrollTo(0, 0); + const hash = this.props.location.hash; + if (hash) { + const element = document.getElementById(hash.substring(1)); + if (element && element.scrollIntoView) { + element.scrollIntoView(); + } + } else { + window.scrollTo(0, 0); + } } } From 6f962ff4cebe2f309effbbd6651a1602f947f025 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 7 May 2019 16:42:04 +0200 Subject: [PATCH 016/118] create links for markdown headings with enabled anchor links --- .../src/MarkdownHeadingRenderer.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js index d7268c2861..eab4bab05a 100644 --- a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js +++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js @@ -1,5 +1,7 @@ // @flow import * as React from "react"; +import { withRouter } from "react-router-dom"; +import { withContextPath } from "./urls"; /** * Adds anchor links to markdown headings. @@ -9,7 +11,8 @@ import * as React from "react"; type Props = { children: React.Node, - level: number + level: number, + location: any }; function flatten(text: string, child: any) { @@ -27,9 +30,18 @@ export function headingToAnchorId(heading: string) { return heading.toLowerCase().replace(/\W/g, "-"); } -export default function MarkdownHeadingRenderer(props: Props) { +function MarkdownHeadingRenderer(props: Props) { const children = React.Children.toArray(props.children); const heading = children.reduce(flatten, ""); const anchorId = headingToAnchorId(heading); - return React.createElement("h" + props.level, {id: anchorId}, props.children); + const headingElement = React.createElement("h" + props.level, {}, props.children); + const href = withContextPath(props.location.pathname + "#" + anchorId); + + return ( + + {headingElement} + + ); } + +export default withRouter(MarkdownHeadingRenderer); From ddaaa1dbe93fd1c737783ab9697b98bf0d121c24 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 7 May 2019 16:49:36 +0200 Subject: [PATCH 017/118] 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 018/118] 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 019/118] 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 020/118] 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 021/118] 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 022/118] 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 023/118] 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 024/118] 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 025/118] 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 026/118] 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 027/118] 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 028/118] 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 029/118] 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 030/118] 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 031/118] 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 032/118] 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 033/118] 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 034/118] 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 035/118] 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 036/118] 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 037/118] 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 038/118] 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 039/118] 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 040/118] 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 041/118] 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 042/118] 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 043/118] 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 044/118] 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 045/118] 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 046/118] 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 047/118] 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 048/118] 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 049/118] 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 050/118] 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 051/118] 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 052/118] 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 053/118] 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 054/118] 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 055/118] 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 056/118] 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 057/118] 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 058/118] 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 059/118] 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 060/118] 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 061/118] 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 062/118] 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 063/118] 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 064/118] 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 065/118] 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 066/118] 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 067/118] 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 068/118] 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 069/118] 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 070/118] 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 071/118] 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 072/118] 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 073/118] 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 074/118] 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 075/118] 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 076/118] 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 077/118] 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 078/118] 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 079/118] 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 080/118] 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 081/118] 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 082/118] 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 083/118] 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 084/118] 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 085/118] 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 086/118] 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 087/118] 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 088/118] 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 089/118] 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 090/118] 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 091/118] 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 092/118] 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 093/118] 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 358819ca61d1e229124be4d368bc63b5c2652fec Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 15:43:48 +0200 Subject: [PATCH 094/118] add missing props --- .../config/roles/components/AvailableVerbs.js | 2 ++ .../roles/components/PermissionRoleDetails.js | 2 +- .../components/PermissionRoleDetailsTable.js | 25 ++++++++++--------- .../roles/components/PermissionRoleTable.js | 17 +++++++------ .../config/roles/components/SystemRoleTag.js | 2 ++ .../roles/containers/CreateRepositoryRole.js | 2 ++ .../roles/containers/RepositoryRoles.js | 1 + 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/scm-ui/src/config/roles/components/AvailableVerbs.js b/scm-ui/src/config/roles/components/AvailableVerbs.js index dbe0b50a77..729e977e95 100644 --- a/scm-ui/src/config/roles/components/AvailableVerbs.js +++ b/scm-ui/src/config/roles/components/AvailableVerbs.js @@ -7,7 +7,9 @@ import injectSheet from "react-jss"; type Props = { role: RepositoryRole, + // context props + classes: any, t: string => string }; diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index 1977ddde2a..d1b48fc8b5 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -4,7 +4,7 @@ import { translate } from "react-i18next"; 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"; +import { Button } from "@scm-manager/ui-components"; type Props = { role: RepositoryRole, diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js index dd0502cb02..28ab5e1f14 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetailsTable.js @@ -6,6 +6,7 @@ import AvailableVerbs from "./AvailableVerbs"; type Props = { role: RepositoryRole, + // context props t: string => string }; @@ -16,18 +17,18 @@ class PermissionRoleDetailsTable extends React.Component<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> - <tr> - <th>{t("repositoryRole.verbs")}</th> - <AvailableVerbs role={role} /> - </tr> + <tr> + <th>{t("repositoryRole.name")}</th> + <td>{role.name}</td> + </tr> + <tr> + <th>{t("repositoryRole.type")}</th> + <td>{role.type}</td> + </tr> + <tr> + <th>{t("repositoryRole.verbs")}</th> + <AvailableVerbs role={role} /> + </tr> </tbody> </table> ); diff --git a/scm-ui/src/config/roles/components/PermissionRoleTable.js b/scm-ui/src/config/roles/components/PermissionRoleTable.js index bb98f7c3e9..dddf30a888 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleTable.js @@ -8,6 +8,7 @@ type Props = { baseUrl: string, roles: RepositoryRole[], + // context props t: string => string }; @@ -17,16 +18,16 @@ class PermissionRoleTable extends React.Component<Props> { return ( <table className="card-table table is-hoverable is-fullwidth"> <thead> - <tr> - <th>{t("repositoryRole.form.name")}</th> - </tr> + <tr> + <th>{t("repositoryRole.form.name")}</th> + </tr> </thead> <tbody> - {roles.map((role, index) => { - return ( - <PermissionRoleRow key={index} baseUrl={baseUrl} role={role} /> - ); - })} + {roles.map((role, index) => { + return ( + <PermissionRoleRow key={index} baseUrl={baseUrl} role={role} /> + ); + })} </tbody> </table> ); diff --git a/scm-ui/src/config/roles/components/SystemRoleTag.js b/scm-ui/src/config/roles/components/SystemRoleTag.js index 0f6b87addc..f9e907de35 100644 --- a/scm-ui/src/config/roles/components/SystemRoleTag.js +++ b/scm-ui/src/config/roles/components/SystemRoleTag.js @@ -6,6 +6,8 @@ import { translate } from "react-i18next"; type Props = { system?: boolean, + + // context props classes: any, t: string => string }; diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index 88f42f8353..75380ada92 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -15,11 +15,13 @@ import { getRepositoryRolesLink, getRepositoryVerbsLink } from "../../../modules/indexResource"; +import type {History} from "history"; type Props = { disabled: boolean, repositoryRolesLink: string, error?: Error, + history: History, //dispatch function addRole: (link: string, role: RepositoryRole, callback?: () => void) => void, diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index f148b120e0..06492add5b 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -36,6 +36,7 @@ type Props = { // context objects t: string => string, history: History, + location: any, // dispatch functions fetchRolesByPage: (link: string, page: number) => void From fca095c3d54578bf313501519bffa92005d7bd20 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 15:44:22 +0200 Subject: [PATCH 095/118] remove unnecessary div containers --- .../roles/components/PermissionRoleDetails.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index d1b48fc8b5..189f3f3972 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -33,18 +33,16 @@ class PermissionRoleDetails extends React.Component<Props> { const { role } = this.props; return ( - <div> + <> <PermissionRoleDetailsTable role={role} /> <hr /> {this.renderEditButton()} - <div className="content"> - <ExtensionPoint - name="repositoryRole.role-details.information" - renderAll={true} - props={{ role }} - /> - </div> - </div> + <ExtensionPoint + name="repositoryRole.role-details.information" + renderAll={true} + props={{ role }} + /> + </> ); } } From e15039559f5f67164d080df554e604e36c33d563 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 15:45:39 +0200 Subject: [PATCH 096/118] remove unsupported query-feature --- .../roles/containers/RepositoryRoles.js | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index 06492add5b..b1cece11bf 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -23,6 +23,7 @@ import { } from "../modules/roles"; import PermissionRoleTable from "../components/PermissionRoleTable"; import { getRolesLink } from "../../../modules/indexResource"; + type Props = { baseUrl: string, roles: RepositoryRole[], @@ -48,27 +49,6 @@ 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; From d3508af920be6bceae4ed5587a7d76b9b66d2c87 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 15:47:20 +0200 Subject: [PATCH 097/118] add subtitle, missing props --- .../config/roles/containers/CreateRepositoryRole.js | 3 ++- .../src/config/roles/containers/EditRepositoryRole.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index 75380ada92..bef37f3603 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -3,7 +3,7 @@ import React from "react"; import RepositoryRoleForm from "./RepositoryRoleForm"; import { connect } from "react-redux"; import { translate } from "react-i18next"; -import { ErrorNotification, Title } from "@scm-manager/ui-components"; +import {ErrorNotification, Subtitle, Title} from "@scm-manager/ui-components"; import { createRole, getCreateRoleFailure, @@ -52,6 +52,7 @@ class CreateRepositoryRole extends React.Component<Props> { return ( <> <Title title={t("repositoryRole.title")} /> + <Subtitle subtitle={t("repositoryRole.createSubtitle")} /> <RepositoryRoleForm disabled={this.props.disabled} submitForm={role => this.createRepositoryRole(role)} diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index deb0a77583..8bf1d5442b 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -8,8 +8,9 @@ import { isModifyRolePending, modifyRole } from "../modules/roles"; -import { ErrorNotification } from "@scm-manager/ui-components"; +import { ErrorNotification, Subtitle } from "@scm-manager/ui-components"; import type { RepositoryRole } from "@scm-manager/ui-types"; +import type { History } from "history"; type Props = { disabled: boolean, @@ -17,6 +18,10 @@ type Props = { repositoryRolesLink: string, error?: Error, + // context objects + t: string => string, + history: History, + //dispatch function updateRole: ( link: string, @@ -36,7 +41,7 @@ class EditRepositoryRole extends React.Component<Props> { }; render() { - const { error } = this.props; + const { error, t } = this.props; if (error) { return <ErrorNotification error={error} />; @@ -44,6 +49,7 @@ class EditRepositoryRole extends React.Component<Props> { return ( <> + <Subtitle subtitle={t("repositoryRole.editSubtitle")} /> <RepositoryRoleForm nameDisabled={true} role={this.props.role} From 155b0f72b938565c0be4ffeaed111fbc633e364c Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 15:51:55 +0200 Subject: [PATCH 098/118] remove unnecessary column block --- .../config/roles/components/AvailableVerbs.js | 4 +-- .../roles/containers/SingleRepositoryRole.js | 36 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/scm-ui/src/config/roles/components/AvailableVerbs.js b/scm-ui/src/config/roles/components/AvailableVerbs.js index 729e977e95..d544557644 100644 --- a/scm-ui/src/config/roles/components/AvailableVerbs.js +++ b/scm-ui/src/config/roles/components/AvailableVerbs.js @@ -1,9 +1,9 @@ //@flow import React from "react"; -import type { RepositoryRole } from "@scm-manager/ui-types"; -import { translate } from "react-i18next"; import { compose } from "redux"; import injectSheet from "react-jss"; +import { translate } from "react-i18next"; +import type { RepositoryRole } from "@scm-manager/ui-types"; type Props = { role: RepositoryRole, diff --git a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js index 3bb951ac1a..4aec3b0b06 100644 --- a/scm-ui/src/config/roles/containers/SingleRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/SingleRepositoryRole.js @@ -81,26 +81,22 @@ class SingleRepositoryRole extends React.Component<Props> { return ( <> <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} - /> - </div> - </div> + <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} + /> </> ); } 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 099/118] Close branch feature/custom_roles_overview From 8fe5da18828308bf2fe379f3a20eb266e1aa8860 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 16:07:13 +0200 Subject: [PATCH 100/118] fix import --- scm-ui/src/config/roles/components/PermissionRoleDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index 189f3f3972..307eb6b48e 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -2,7 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import type { RepositoryRole } from "@scm-manager/ui-types"; -import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable"; import { Button } from "@scm-manager/ui-components"; From 1f25ba6040c40f28a23ba1438cb5b68a70e14210 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 16:10:14 +0200 Subject: [PATCH 101/118] remove unnecessary disabled prop --- scm-ui/src/config/containers/Config.js | 1 - scm-ui/src/config/roles/containers/CreateRepositoryRole.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 496dce4611..058df06c28 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -74,7 +74,6 @@ class Config extends React.Component<Props> { path={`${url}/roles/create`} render={() => ( <CreateRepositoryRole - disabled={false} history={this.props.history} /> )} diff --git a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js index bef37f3603..55b141469f 100644 --- a/scm-ui/src/config/roles/containers/CreateRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/CreateRepositoryRole.js @@ -18,7 +18,6 @@ import { import type {History} from "history"; type Props = { - disabled: boolean, repositoryRolesLink: string, error?: Error, history: History, @@ -54,7 +53,6 @@ class CreateRepositoryRole extends React.Component<Props> { <Title title={t("repositoryRole.title")} /> <Subtitle subtitle={t("repositoryRole.createSubtitle")} /> <RepositoryRoleForm - disabled={this.props.disabled} submitForm={role => this.createRepositoryRole(role)} /> </> From 0b76247345510912b1b06dac27dd75b65935c2d0 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 16:14:35 +0200 Subject: [PATCH 102/118] remove nameDisabled prop and added disable check --- scm-ui/src/config/roles/containers/EditRepositoryRole.js | 1 - scm-ui/src/config/roles/containers/RepositoryRoleForm.js | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index 8bf1d5442b..f4badd9193 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -51,7 +51,6 @@ class EditRepositoryRole extends React.Component<Props> { <> <Subtitle subtitle={t("repositoryRole.editSubtitle")} /> <RepositoryRoleForm - nameDisabled={true} role={this.props.role} submitForm={role => this.updateRepositoryRole(role)} /> diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index 43d901a285..7b7e628f87 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -19,7 +19,6 @@ import { type Props = { role?: RepositoryRole, loading?: boolean, - nameDisabled: boolean, availableVerbs: string[], verbsLink: string, submitForm: RepositoryRole => void, @@ -103,7 +102,7 @@ class RepositoryRoleForm extends React.Component<Props, State> { }; render() { - const { loading, availableVerbs, nameDisabled, t } = this.props; + const { loading, availableVerbs, t } = this.props; const { role } = this.state; const verbSelectBoxes = !availableVerbs @@ -126,7 +125,7 @@ class RepositoryRoleForm extends React.Component<Props, State> { label={t("repositoryRole.create.name")} onChange={this.handleNameChange} value={role.name ? role.name : ""} - disabled={nameDisabled} + disabled={!!this.props.role} /> </div> </div> From 7631d623be4701ed0b923db6e0b25a701526b8ea Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 16:16:37 +0200 Subject: [PATCH 103/118] remove duplicated indexResource --- scm-ui/src/config/roles/containers/RepositoryRoles.js | 4 ++-- scm-ui/src/modules/indexResource.js | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index b1cece11bf..a9dd967897 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -22,7 +22,7 @@ import { getFetchRolesFailure } from "../modules/roles"; import PermissionRoleTable from "../components/PermissionRoleTable"; -import { getRolesLink } from "../../../modules/indexResource"; +import { getRepositoryRolesLink } from "../../../modules/indexResource"; type Props = { baseUrl: string, @@ -104,7 +104,7 @@ const mapStateToProps = (state, ownProps) => { const page = urls.getPageFromMatch(match); const canAddRoles = isPermittedToCreateRoles(state); const list = selectListAsCollection(state); - const rolesLink = getRolesLink(state); + const rolesLink = getRepositoryRolesLink(state); return { roles, diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index 2c6805d207..12126e4841 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -159,10 +159,6 @@ export function getSvnConfigLink(state: Object) { return getLink(state, "svnConfig"); } -export function getRolesLink(state: Object) { - return getLink(state, "repositoryRoles"); -} - export function getUserAutoCompleteLink(state: Object): string { const link = getLinkCollection(state, "autocomplete").find( i => i.name === "users" From 5007b257ba05fa46f27d5cee5359ace9cfd9a2a7 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 16:22:44 +0200 Subject: [PATCH 104/118] remove unnecessary div containers --- .../roles/containers/RepositoryRoleForm.js | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index 7b7e628f87..9af8ac75f8 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -118,28 +118,20 @@ class RepositoryRoleForm extends React.Component<Props, State> { return ( <form onSubmit={this.submit}> - <div className="columns"> - <div className="column"> - <InputField - name="name" - label={t("repositoryRole.create.name")} - onChange={this.handleNameChange} - value={role.name ? role.name : ""} - disabled={!!this.props.role} - /> - </div> - </div> - <>{verbSelectBoxes}</> + <InputField + name="name" + label={t("repositoryRole.create.name")} + onChange={this.handleNameChange} + value={role.name ? role.name : ""} + disabled={!!this.props.role} + /> + {verbSelectBoxes} <hr /> - <div className="columns"> - <div className="column"> - <SubmitButton - loading={loading} - label={t("repositoryRole.form.submit")} - disabled={!this.isValid()} - /> - </div> - </div> + <SubmitButton + loading={loading} + label={t("repositoryRole.form.submit")} + disabled={!this.isValid()} + /> </form> ); } From 6b91faa4e2273ad8a8c5b268064803480a527f94 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 16:31:01 +0200 Subject: [PATCH 105/118] fix dispatch function and loading prop --- .../config/roles/containers/EditRepositoryRole.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/scm-ui/src/config/roles/containers/EditRepositoryRole.js b/scm-ui/src/config/roles/containers/EditRepositoryRole.js index f4badd9193..6dc783bec3 100644 --- a/scm-ui/src/config/roles/containers/EditRepositoryRole.js +++ b/scm-ui/src/config/roles/containers/EditRepositoryRole.js @@ -23,21 +23,16 @@ type Props = { history: History, //dispatch function - updateRole: ( - link: string, - role: RepositoryRole, - callback?: () => void - ) => void + updateRole: (role: RepositoryRole, callback?: () => void) => void }; class EditRepositoryRole extends React.Component<Props> { - repositoryRoleUpdated = (role: RepositoryRole) => { - const { history } = this.props; - history.push("/config/roles/"); + repositoryRoleUpdated = () => { + this.props.history.push("/config/roles/"); }; updateRepositoryRole = (role: RepositoryRole) => { - this.props.updateRole(role, () => this.repositoryRoleUpdated(role)); + this.props.updateRole(role, this.repositoryRoleUpdated); }; render() { @@ -60,7 +55,7 @@ class EditRepositoryRole extends React.Component<Props> { } const mapStateToProps = (state, ownProps) => { - const loading = isModifyRolePending(state); + const loading = isModifyRolePending(state, ownProps.role.name); const error = getModifyRoleFailure(state, ownProps.role.name); return { From 15cb29e1fc98e68fe5ba2b4718e6e49108906b3b Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 16:55:21 +0200 Subject: [PATCH 106/118] clarified translations --- scm-ui/public/locales/en/config.json | 28 +++++++++---------- .../roles/components/PermissionRoleDetails.js | 2 +- .../roles/components/PermissionRoleTable.js | 2 +- .../config/roles/components/SystemRoleTag.js | 2 +- .../roles/containers/RepositoryRoleForm.js | 9 ++++-- .../roles/containers/RepositoryRoles.js | 10 ++++--- 6 files changed, 29 insertions(+), 24 deletions(-) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index bdb245544a..66ddea3ee2 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -9,30 +9,28 @@ "repositoryRole": { "navLink": "Permission Roles", "title": "Permission Roles", - "noPermissionRoles": "No permission roles found.", - "system": "System", - "createButton": "Create Permission Role", + "errorTitle": "Error", + "errorSubtitle": "Unknown Permission Role Error", + "createSubtitle": "Create Permission Role", + "editSubtitle": "Edit Permission Role", + "overview": { + "title": "Overview of all permission roles", + "noPermissionRoles": "No permission roles found.", + "createButton": "Create Permission Role" + }, + "details": { + "editButton": "Edit" + }, "name": "Name", "type": "Type", "verbs": "Verbs", - "edit": "Edit Permission Role", - "button": { - "edit": "Edit" - }, - "create": { - "name": "Name" - }, + "system": "System", "form": { - "subtitle": "Edit Permission Role", "name": "Name", "permissions": "Permissions", "submit": "Save" } }, - "role": { - "name": "Name", - "system": "System" - }, "config-form": { "submit": "Submit", "submit-success-notification": "Configuration changed successfully!", diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index 307eb6b48e..6f5121e227 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -20,7 +20,7 @@ class PermissionRoleDetails extends React.Component<Props> { if (!!this.props.role._links.update) { return ( <Button - label={t("repositoryRole.button.edit")} + label={t("repositoryRole.details.editButton")} link={`${url}/edit`} color="primary" /> diff --git a/scm-ui/src/config/roles/components/PermissionRoleTable.js b/scm-ui/src/config/roles/components/PermissionRoleTable.js index dddf30a888..13c5d775d1 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleTable.js +++ b/scm-ui/src/config/roles/components/PermissionRoleTable.js @@ -19,7 +19,7 @@ class PermissionRoleTable extends React.Component<Props> { <table className="card-table table is-hoverable is-fullwidth"> <thead> <tr> - <th>{t("repositoryRole.form.name")}</th> + <th>{t("repositoryRole.name")}</th> </tr> </thead> <tbody> diff --git a/scm-ui/src/config/roles/components/SystemRoleTag.js b/scm-ui/src/config/roles/components/SystemRoleTag.js index f9e907de35..8aad57eddc 100644 --- a/scm-ui/src/config/roles/components/SystemRoleTag.js +++ b/scm-ui/src/config/roles/components/SystemRoleTag.js @@ -26,7 +26,7 @@ class SystemRoleTag extends React.Component<Props> { if (system) { return ( <span className={classNames("tag is-dark", classes.tag)}> - {t("role.system")} + {t("repositoryRole.system")} </span> ); } diff --git a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js index 9af8ac75f8..9269af3838 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoleForm.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoleForm.js @@ -120,12 +120,17 @@ class RepositoryRoleForm extends React.Component<Props, State> { <form onSubmit={this.submit}> <InputField name="name" - label={t("repositoryRole.create.name")} + label={t("repositoryRole.form.name")} onChange={this.handleNameChange} value={role.name ? role.name : ""} disabled={!!this.props.role} /> - {verbSelectBoxes} + <div className="field"> + <label className="label"> + {t("repositoryRole.form.permissions")} + </label> + {verbSelectBoxes} + </div> <hr /> <SubmitButton loading={loading} diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index a9dd967897..689c8cf1cc 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -7,6 +7,7 @@ import type { History } from "history"; import type { RepositoryRole, PagedCollection } from "@scm-manager/ui-types"; import { Title, + Subtitle, Loading, Notification, LinkPaginator, @@ -57,11 +58,12 @@ class RepositoryRoles extends React.Component<Props> { } return ( - <div> + <> <Title title={t("repositoryRole.title")} /> + <Subtitle subtitle={t("repositoryRole.overview.title")} /> {this.renderPermissionsTable()} {this.renderCreateButton()} - </div> + </> ); } @@ -77,7 +79,7 @@ class RepositoryRoles extends React.Component<Props> { } return ( <Notification type="info"> - {t("repositoryRole.noPermissionRoles")} + {t("repositoryRole.overview.noPermissionRoles")} </Notification> ); } @@ -87,7 +89,7 @@ class RepositoryRoles extends React.Component<Props> { if (canAddRoles) { return ( <CreateButton - label={t("repositoryRole.createButton")} + label={t("repositoryRole.overview.createButton")} link={`${baseUrl}/create`} /> ); From 1ee76d2c31eaf6ef593715f799e96431b9da6691 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 17:04:04 +0200 Subject: [PATCH 107/118] corrected remove unsupported query-feature commit --- .../roles/containers/RepositoryRoles.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scm-ui/src/config/roles/containers/RepositoryRoles.js b/scm-ui/src/config/roles/containers/RepositoryRoles.js index 689c8cf1cc..2de1d3d74c 100644 --- a/scm-ui/src/config/roles/containers/RepositoryRoles.js +++ b/scm-ui/src/config/roles/containers/RepositoryRoles.js @@ -50,6 +50,26 @@ 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 + ); + } + } + }; + render() { const { t, loading } = this.props; From e4963fb9de588e4b678e718218d8d533606852cc Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 17:04:40 +0200 Subject: [PATCH 108/118] fix activation of nav on roles/create page --- scm-ui/src/config/containers/Config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 058df06c28..53f00ea3dc 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -103,6 +103,7 @@ class Config extends React.Component<Props> { to={`${url}/roles/`} label={t("repositoryRole.navLink")} activeWhenMatch={this.matchesRoles} + activeOnlyWhenExact={false} /> <ExtensionPoint name="config.navigation" From 7343edb819b5f30fbbda52c4cbda6a9ed0293ade Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 17 May 2019 17:13:16 +0200 Subject: [PATCH 109/118] add german translations --- scm-ui/public/locales/de/config.json | 26 ++++++++----------- scm-ui/public/locales/en/config.json | 6 ++--- .../roles/components/PermissionRoleDetails.js | 2 +- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index d26552c30d..3fc61a9b47 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -9,30 +9,26 @@ "repositoryRole": { "navLink": "Berechtigungsrollen", "title": "Berechtigungsrollen", - "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", - "system": "System", - "createButton": "Berechtigungsrolle erstellen", + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Berechtigungsrollen Fehler", + "createSubtitle": "Berechtigungsrolle erstellen", + "editSubtitle": "Berechtigungsrolle bearbeiten", + "overview": { + "title": "Übersicht aller verfügbaren Berechtigungsrollen", + "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", + "createButton": "Berechtigungsrolle erstellen" + }, + "editButton": "Bearbeiten", "name": "Name", "type": "Typ", "verbs": "Berechtigungen", - "button": { - "edit": "Bearbeiten" - }, - "create": { - "name": "Name" - }, - "edit": "Berechtigungsrolle bearbeiten", + "system": "System", "form": { - "subtitle": "Berechtigungsrolle bearbeiten", "name": "Name", "permissions": "Berechtigungen", "submit": "Speichern" } }, - "role": { - "name": "Name", - "system": "System" - }, "deleteRole" : { "button": "Löschen", "subtitle": "Berechtigungsrolle löschen", diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index a57a962ced..d7ffa3d229 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -18,9 +18,7 @@ "noPermissionRoles": "No permission roles found.", "createButton": "Create Permission Role" }, - "details": { - "editButton": "Edit" - }, + "editButton": "Edit", "name": "Name", "type": "Type", "verbs": "Permissions", @@ -31,7 +29,7 @@ "submit": "Save" } }, - "deleteRole" : { + "deleteRole": { "button": "Delete", "subtitle": "Delete Permission Role", "confirmAlert": { diff --git a/scm-ui/src/config/roles/components/PermissionRoleDetails.js b/scm-ui/src/config/roles/components/PermissionRoleDetails.js index 6f5121e227..610bc98b6a 100644 --- a/scm-ui/src/config/roles/components/PermissionRoleDetails.js +++ b/scm-ui/src/config/roles/components/PermissionRoleDetails.js @@ -20,7 +20,7 @@ class PermissionRoleDetails extends React.Component<Props> { if (!!this.props.role._links.update) { return ( <Button - label={t("repositoryRole.details.editButton")} + label={t("repositoryRole.editButton")} link={`${url}/edit`} color="primary" /> From ca36a0a5f4434f9bbb131b24fe2d1299d90418a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Mon, 20 May 2019 14:26:10 +0200 Subject: [PATCH 110/118] Move event bus module to bootstrap context --- scm-webapp/src/main/java/sonia/scm/ScmContextListener.java | 1 - .../main/java/sonia/scm/boot/BootstrapContextListener.java | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 6dbc33af03..4f61ebaa17 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -133,7 +133,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList moduleList.add(new ResteasyModule()); moduleList.add(new ScmInitializerModule()); - moduleList.add(new ScmEventBusModule()); moduleList.add(new EagerSingletonModule()); moduleList.add(ShiroWebModule.guiceFilterModule()); moduleList.add(new WebElementModule(pluginLoader)); diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index b2dcbbba4a..86e62d8f94 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -41,6 +41,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContext; import sonia.scm.ScmContextListener; +import sonia.scm.ScmEventBusModule; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; import sonia.scm.plugin.DefaultPluginLoader; @@ -149,8 +150,9 @@ public class BootstrapContextListener implements ServletContextListener { Module scmContextListenerModule = new ScmContextListenerModule(); BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); + ScmEventBusModule scmEventBusModule = new ScmEventBusModule(); - Injector bootstrapInjector = Guice.createInjector(bootstrapModule, scmContextListenerModule); + Injector bootstrapInjector = Guice.createInjector(bootstrapModule, scmContextListenerModule, scmEventBusModule); processUpdates(pluginLoader, bootstrapInjector); From af468898b9dff732936b7ab3685cfc7c939cdecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Mon, 20 May 2019 14:30:57 +0200 Subject: [PATCH 111/118] Reject permission requests with missing permissions --- .../java/sonia/scm/api/v2/resources/PermissionListDto.java | 3 +++ .../sonia/scm/api/v2/resources/UserPermissionResource.java | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java index 23d57f4d8e..8d48eafc82 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java @@ -7,12 +7,15 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import javax.validation.constraints.NotNull; + @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class PermissionListDto extends HalRepresentation { + @NotNull private String[] permissions; @Override diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index 2b02104646..a961dfaa0e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -8,6 +8,7 @@ import sonia.scm.security.PermissionDescriptor; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -69,7 +70,7 @@ public class UserPermissionResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) { + public Response overwritePermissions(@PathParam("id") String id, @Valid PermissionListDto newPermissions) { Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) .map(PermissionDescriptor::new) .collect(Collectors.toList()); From be9ce826dce00df63e2640a9f946d1f16def2a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Tue, 21 May 2019 09:13:49 +0200 Subject: [PATCH 112/118] Move further listener modules to bootstrap module --- .../src/main/java/sonia/scm/ScmContextListener.java | 2 -- .../sonia/scm/boot/BootstrapContextListener.java | 13 ++++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 4f61ebaa17..40fb345caa 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -132,8 +132,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList List<Module> moduleList = Lists.newArrayList(); moduleList.add(new ResteasyModule()); - moduleList.add(new ScmInitializerModule()); - moduleList.add(new EagerSingletonModule()); moduleList.add(ShiroWebModule.guiceFilterModule()); moduleList.add(new WebElementModule(pluginLoader)); moduleList.add(new ScmServletModule(context, pluginLoader, overrides)); diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 86e62d8f94..69e70828a2 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -39,9 +39,11 @@ import com.google.inject.Module; import com.google.inject.assistedinject.FactoryModuleBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.EagerSingletonModule; import sonia.scm.SCMContext; import sonia.scm.ScmContextListener; import sonia.scm.ScmEventBusModule; +import sonia.scm.ScmInitializerModule; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; import sonia.scm.plugin.DefaultPluginLoader; @@ -150,9 +152,18 @@ public class BootstrapContextListener implements ServletContextListener { Module scmContextListenerModule = new ScmContextListenerModule(); BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); + ScmInitializerModule scmInitializerModule = new ScmInitializerModule(); + EagerSingletonModule eagerSingletonModule = new EagerSingletonModule(); ScmEventBusModule scmEventBusModule = new ScmEventBusModule(); - Injector bootstrapInjector = Guice.createInjector(bootstrapModule, scmContextListenerModule, scmEventBusModule); + Injector bootstrapInjector = + Guice.createInjector( + bootstrapModule, + scmContextListenerModule, + scmEventBusModule, + scmInitializerModule, + eagerSingletonModule + ); processUpdates(pluginLoader, bootstrapInjector); From f3d7727198828ac7427e7550d032441a18a348a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Mon, 20 May 2019 16:34:22 +0200 Subject: [PATCH 113/118] Add documentation --- .../java/sonia/scm/migration/UpdateStep.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java index eaa6d8d549..ed5f60a630 100644 --- a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java @@ -3,11 +3,80 @@ package sonia.scm.migration; import sonia.scm.plugin.ExtensionPoint; import sonia.scm.version.Version; +/** + * This is the main interface for data migration/update. Using this interface, SCM-Manager provides the possibility to + * change data structures between versions for a given type of data. + * <p>The data type can be an arbitrary string, but it is considered a best practice to use a qualified name, for + * example + * <ul> + * <li><code>com.example.myPlugin.configuration</code></li> for data in plugins, or + * <li><code>com.cloudogu.scm.repository</code></li> for core data structures. + * </ul> + * </p> + * <p>The version is unrelated to other versions and therefore can be chosen freely, so that a data type can be updated + * without in various ways independent of other data types or the official version of the plugin or the core. + * A coordination between different data types and their versions is only necessary, when update steps of different data + * types rely on each other. If a update step of data type <i>A</i> has to run <b>before</b> another step for data type + * <i>B</i>, the version number of the second step has to be greater in regards to {@link Version#compareTo(Version)}. + * </p> + * <p>The algorithm looks something like this:<br> + * Whenever the SCM-Manager starts, + * <ul> + * <li>it creates a so called <i>bootstrap guice context</i>, that contains + * <ul> + * <li>a {@link sonia.scm.security.KeyGenerator},</li> + * <li>the {@link sonia.scm.repository.RepositoryLocationResolver},</li> + * <li>the {@link sonia.scm.io.FileSystem},</li> + * <li>the {@link sonia.scm.security.CipherHandler},</li> + * <li>a {@link sonia.scm.store.ConfigurationStoreFactory},</li> + * <li>a {@link sonia.scm.store.ConfigurationEntryStoreFactory},</li> + * <li>a {@link sonia.scm.store.DataStoreFactory},</li> + * <li>a {@link sonia.scm.store.BlobStoreFactory}, and</li> + * <li>the {@link sonia.scm.plugin.PluginLoader}.</li> + * </ul> + * Mind, that there are no DAOs, Managers or the like available at this time! + * </li> + * <li>It then checks whether there are instances of this interface that have not run before, that is either + * <ul> + * <li>their version number given by {@link #getTargetVersion()} is bigger than the last recorded target version of an + * executed update step for the data type given by {@link #getAffectedDataType()}, or + * </li> + * <li>there is no version number known for the given data type. + * </li> + * </ul> + * These are the <i>relevant</i> update steps. + * </li> + * <li>These relevant update steps are then sorted ascending by their target version given by + * {@link #getTargetVersion()}. + * </li> + * <li>Finally, these sorted steps are executed one after another calling {@link #doUpdate()} of each step, updating the + * version for the data type accordingly. + * </li> + * <li>If all works well, SCM-Manager then creates the runtime guice context by loading all further modules.</li> + * <li>If any of the update steps fails, the whole process is interrupted and SCM-Manager will not start up and will + * not record the version number of this update step. + * </li> + * </ul> + * </p> + */ @ExtensionPoint public interface UpdateStep { + /** + * Implement this to update the data to the new version. If any {@link Exception} is thrown, SCM-Manager will not + * start up. + */ void doUpdate() throws Exception; + /** + * Declares the new version of the data type given by {@link #getAffectedDataType()}. A update step will only be + * executed, when this version is bigger than the last recorded version for its data type according to + * {@link Version#compareTo(Version)} + */ Version getTargetVersion(); + /** + * Declares the data type this update step will take care of. This should be a qualified name, like + * <code>com.example.myPlugin.configuration</code>. + */ String getAffectedDataType(); } From 033c213cf223f9e83985cf67223872e97ceff9b5 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Tue, 21 May 2019 10:22:53 +0200 Subject: [PATCH 114/118] fix scrolling for markdown content, which is loaded asynchronous The code to find and scroll to the anchor is now moved from the ScrollToTop component to the MarkdownView. The anchor with the id from location hash, is searched after the MarkdownView and all its children finished rendering. --- .../ui-components/src/MarkdownView.js | 42 +++++++++++++++---- scm-ui/src/containers/ScrollToTop.js | 10 +---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/MarkdownView.js b/scm-ui-components/packages/ui-components/src/MarkdownView.js index 164c06b84a..4620004f59 100644 --- a/scm-ui-components/packages/ui-components/src/MarkdownView.js +++ b/scm-ui-components/packages/ui-components/src/MarkdownView.js @@ -4,12 +4,17 @@ import SyntaxHighlighter from "./SyntaxHighlighter"; import Markdown from "react-markdown/with-html"; import {binder} from "@scm-manager/ui-extensions"; import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; +import { withRouter } from "react-router-dom"; + type Props = { content: string, renderContext?: Object, renderers?: Object, - enableAnchorHeadings: boolean + enableAnchorHeadings: boolean, + + // context props + location: any }; class MarkdownView extends React.Component<Props> { @@ -18,10 +23,27 @@ class MarkdownView extends React.Component<Props> { enableAnchorHeadings: false }; + contentRef: ?HTMLDivElement; + constructor(props: Props) { super(props); } + componentDidUpdate() { + // we have to use componentDidUpdate, because we have to wait until all + // children are rendered and componentDidMount is called before the + // markdown content was rendered. + const hash = this.props.location.hash; + if (this.contentRef && hash) { + // we query only child elements, to avoid strange scrolling with multiple + // markdown elements on one page. + const element = this.contentRef.querySelector(hash); + if (element && element.scrollIntoView) { + element.scrollIntoView(); + } + } + } + render() { const {content, renderers, renderContext, enableAnchorHeadings} = this.props; @@ -45,15 +67,17 @@ class MarkdownView extends React.Component<Props> { } return ( - <Markdown - className="content" - skipHtml={true} - escapeHtml={true} - source={content} - renderers={rendererList} - /> + <div ref={el => (this.contentRef = el)}> + <Markdown + className="content" + skipHtml={true} + escapeHtml={true} + source={content} + renderers={rendererList} + /> + </div> ); } } -export default MarkdownView; +export default withRouter(MarkdownView); diff --git a/scm-ui/src/containers/ScrollToTop.js b/scm-ui/src/containers/ScrollToTop.js index 77054b8463..d48ea6531a 100644 --- a/scm-ui/src/containers/ScrollToTop.js +++ b/scm-ui/src/containers/ScrollToTop.js @@ -11,15 +11,7 @@ type Props = { class ScrollToTop extends React.Component<Props> { componentDidUpdate(prevProps) { if (this.props.location.pathname !== prevProps.location.pathname) { - const hash = this.props.location.hash; - if (hash) { - const element = document.getElementById(hash.substring(1)); - if (element && element.scrollIntoView) { - element.scrollIntoView(); - } - } else { - window.scrollTo(0, 0); - } + window.scrollTo(0, 0); } } From e2d05fd0149e231d501aa59e6e91adca5c12e759 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Tue, 21 May 2019 13:38:16 +0000 Subject: [PATCH 115/118] Close branch feature/markdown_anchor_links From dd3aab40eb4558cdb488b8f8090bbcfd238dee5f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer <rene.pfeuffer@cloudogu.com> Date: Wed, 22 May 2019 07:43:45 +0000 Subject: [PATCH 116/118] Close branch bugfix/repo_roles From 9abafb53546acc392c3561b1ce2bf17dc3b641ce Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 23 May 2019 15:45:58 +0200 Subject: [PATCH 117/118] use PluginLoader instead of DefaultPluginLoader and split large contextInitialized method --- .../scm/boot/BootstrapContextListener.java | 60 ++++++++++--------- .../java/sonia/scm/boot/BootstrapModule.java | 3 +- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 69e70828a2..3af8a76650 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -50,6 +50,7 @@ import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.Plugin; import sonia.scm.plugin.PluginException; import sonia.scm.plugin.PluginLoadException; +import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.SmpArchive; @@ -137,6 +138,19 @@ public class BootstrapContextListener implements ServletContextListener { File pluginDirectory = getPluginDirectory(); + createContextListener(pluginDirectory); + + contextListener.contextInitialized(sce); + + // register for restart events + if (!registered && (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) { + logger.info("register for restart events"); + ScmEventBus.getInstance().register(this); + registered = true; + } + } + + private void createContextListener(File pluginDirectory) { try { if (!isCorePluginExtractionDisabled()) { extractCorePlugins(context, pluginDirectory); @@ -148,22 +162,9 @@ public class BootstrapContextListener implements ServletContextListener { Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath()); - DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins); + PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins); - Module scmContextListenerModule = new ScmContextListenerModule(); - BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); - ScmInitializerModule scmInitializerModule = new ScmInitializerModule(); - EagerSingletonModule eagerSingletonModule = new EagerSingletonModule(); - ScmEventBusModule scmEventBusModule = new ScmEventBusModule(); - - Injector bootstrapInjector = - Guice.createInjector( - bootstrapModule, - scmContextListenerModule, - scmEventBusModule, - scmInitializerModule, - eagerSingletonModule - ); + Injector bootstrapInjector = createBootstrapInjector(pluginLoader); processUpdates(pluginLoader, bootstrapInjector); @@ -171,19 +172,25 @@ public class BootstrapContextListener implements ServletContextListener { } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } - - contextListener.contextInitialized(sce); - - // register for restart events - if (!registered - && (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) { - logger.info("register for restart events"); - ScmEventBus.getInstance().register(this); - registered = true; - } } - private void processUpdates(DefaultPluginLoader pluginLoader, Injector bootstrapInjector) { + private Injector createBootstrapInjector(PluginLoader pluginLoader) { + Module scmContextListenerModule = new ScmContextListenerModule(); + BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); + ScmInitializerModule scmInitializerModule = new ScmInitializerModule(); + EagerSingletonModule eagerSingletonModule = new EagerSingletonModule(); + ScmEventBusModule scmEventBusModule = new ScmEventBusModule(); + + return Guice.createInjector( + bootstrapModule, + scmContextListenerModule, + scmEventBusModule, + scmInitializerModule, + eagerSingletonModule + ); + } + + private void processUpdates(PluginLoader pluginLoader, Injector bootstrapInjector) { Injector updateInjector = bootstrapInjector.createChildInjector(new UpdateStepModule(pluginLoader)); UpdateEngine updateEngine = updateInjector.getInstance(UpdateEngine.class); @@ -403,7 +410,6 @@ public class BootstrapContextListener implements ServletContextListener { private static class ScmContextListenerModule extends AbstractModule { @Override protected void configure() { - install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java index 2c7098a951..57c05b9d21 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java @@ -9,7 +9,6 @@ import sonia.scm.SCMContext; import sonia.scm.SCMContextProvider; import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; -import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; @@ -33,7 +32,7 @@ public class BootstrapModule extends AbstractModule { private final ClassOverrides overrides; private final PluginLoader pluginLoader; - BootstrapModule(DefaultPluginLoader pluginLoader) { + BootstrapModule(PluginLoader pluginLoader) { this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader()); this.pluginLoader = pluginLoader; } From 2cd563b975c2d82cd097c204b4398ee4d249f221 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 23 May 2019 15:04:37 +0000 Subject: [PATCH 118/118] Close branch feature/migration