From 012ac34a6accac5ce22aa6284decbfc11bbd5a76 Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Tue, 14 Aug 2018 16:44:42 +0200 Subject: [PATCH 1/7] #8771 create the Permission DTO --- .../java/sonia/scm/repository/Permission.java | 81 ++++++++----------- .../scm/api/v2/resources/PermissionDto.java | 26 ++++++ .../api/v2/resources/PermissionTypeDto.java | 26 ++++++ 3 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/Permission.java index b5de810f75..2a1f34aa85 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Permission.java +++ b/scm-core/src/main/java/sonia/scm/repository/Permission.java @@ -1,32 +1,32 @@ -/** - * 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 - * +/* + 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 + */ @@ -57,10 +57,11 @@ import javax.xml.bind.annotation.XmlRootElement; public class Permission implements PermissionObject, Serializable { - /** Field description */ private static final long serialVersionUID = -2915175031430884040L; - //~--- constructors --------------------------------------------------------- + private boolean groupPermission = false; + private String name; + private PermissionType type = PermissionType.READ; /** * Constructs a new {@link Permission}. @@ -153,12 +154,7 @@ public class Permission implements PermissionObject, Serializable return Objects.hashCode(name, type, groupPermission); } - /** - * Method description - * - * - * @return - */ + @Override public String toString() { @@ -242,15 +238,4 @@ public class Permission implements PermissionObject, Serializable { this.type = type; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private boolean groupPermission = false; - - /** Field description */ - private String name; - - /** Field description */ - private PermissionType type = PermissionType.READ; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java new file mode 100644 index 0000000000..ebd49423ab --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class PermissionDto extends HalRepresentation { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private PermissionTypeDto type = PermissionTypeDto.READ; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String name; + + private boolean groupPermission = false; + + + @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/PermissionTypeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java new file mode 100644 index 0000000000..7b280f9f1f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +/** + * Type of permissionPrefix for a {@link RepositoryDto}. + * + * @author mkarray + */ + +public enum PermissionTypeDto { + + /** + * read permission + */ + READ, + + /** + * read and write permission + */ + WRITE, + + /** + * read, write and manage the properties and permissions + */ + OWNER + +} From f79975b18d6608a75f8d6fa95b66a828ca619c51 Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Mon, 20 Aug 2018 18:16:14 +0200 Subject: [PATCH 2/7] #8771 Permission endpoints --- .../PermissionAlreadyExistsException.java | 11 + .../PermissionNotFoundException.java | 12 + .../main/java/sonia/scm/web/VndMediaType.java | 1 + scm-webapp/pom.xml | 20 +- .../scm/api/rest/StatusExceptionMapper.java | 81 ++-- .../AuthorizationExceptionMapper.java | 50 ++ .../scm/api/v2/resources/MapperModule.java | 2 + ...ermissionAlreadyExistsExceptionMapper.java | 50 ++ .../PermissionCollectionResource.java | 18 - .../scm/api/v2/resources/PermissionDto.java | 17 +- .../PermissionDtoToPermissionMapper.java | 21 + .../PermissionNotFoundExceptionMapper.java | 50 ++ .../v2/resources/PermissionRootResource.java | 230 ++++++++- .../PermissionToPermissionDtoMapper.java | 37 ++ .../api/v2/resources/PermissionTypeDto.java | 26 -- .../RepositoryNotFoundExceptionMapper.java | 50 ++ .../RepositoryToRepositoryDtoMapper.java | 2 +- .../scm/api/v2/resources/ResourceLinks.java | 31 +- .../resources/PermissionRootResourceTest.java | 437 ++++++++++++++++++ .../api/v2/resources/ResourceLinksMock.java | 2 +- 20 files changed, 1035 insertions(+), 113 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java new file mode 100644 index 0000000000..aeaf64a3e9 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java @@ -0,0 +1,11 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionAlreadyExistsException extends RepositoryException { + + public PermissionAlreadyExistsException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} is already exists", permissionName, repository.getNamespace(), repository.getName())); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java new file mode 100644 index 0000000000..9e1b644faa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java @@ -0,0 +1,12 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionNotFoundException extends RepositoryException{ + + + public PermissionNotFoundException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} does not exists", permissionName,repository.getNamespace(), repository.getName() )); + } + +} 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 1e439a6a16..afb1417670 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -16,6 +16,7 @@ public class VndMediaType { public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; + public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index d0003c1f7c..c622042061 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -276,7 +276,25 @@ ${jersey-client.version} test - + + + org.junit.jupiter + junit-jupiter-api + 5.2.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.2.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.2.0 + test + diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java index 08d6b984d2..70b0d0ce2e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java @@ -1,30 +1,30 @@ -/** - * 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 - * +/* + 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 + */ @@ -56,14 +56,14 @@ public class StatusExceptionMapper private static final Logger logger = LoggerFactory.getLogger(StatusExceptionMapper.class); - //~--- constructors --------------------------------------------------------- + private final Response.Status status; + private final Class type; /** - * Constructs ... + * Map an Exception to a HTTP Response * - * - * @param type - * @param status + * @param type the exception class + * @param status the http status to be mapped */ public StatusExceptionMapper(Class type, Response.Status status) { @@ -71,15 +71,12 @@ public class StatusExceptionMapper this.status = status; } - //~--- methods -------------------------------------------------------------- - /** - * Method description + * provide a http responses from an exception * + * @param exception the thrown exception * - * @param exception - * - * @return + * @return the http response with the exception presentation */ @Override public Response toResponse(E exception) @@ -95,12 +92,4 @@ public class StatusExceptionMapper return Response.status(status).build(); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final Response.Status status; - - /** Field description */ - private final Class type; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java new file mode 100644 index 0000000000..bf00bbfc5e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java @@ -0,0 +1,50 @@ +/* + 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.api.v2.resources; + + +import org.apache.shiro.authz.AuthorizationException; +import sonia.scm.api.rest.StatusExceptionMapper; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class AuthorizationExceptionMapper extends StatusExceptionMapper { + + public AuthorizationExceptionMapper() { + super(AuthorizationException.class, Response.Status.UNAUTHORIZED); + } +} 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 0ac6929689..0605d943e7 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 @@ -25,6 +25,8 @@ public class MapperModule extends AbstractModule { bind(RepositoryTypeCollectionToDtoMapper.class); bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); + bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); + bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass()); bind(UriInfoStore.class).in(ServletScopes.REQUEST); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java new file mode 100644 index 0000000000..0cf83f097a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java @@ -0,0 +1,50 @@ +/* + 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.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionAlreadyExistsException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class PermissionAlreadyExistsExceptionMapper extends StatusExceptionMapper { + + public PermissionAlreadyExistsExceptionMapper() { + super(PermissionAlreadyExistsException.class, Response.Status.CONFLICT); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java deleted file mode 100644 index 6c4b52c16d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java +++ /dev/null @@ -1,18 +0,0 @@ -package sonia.scm.api.v2.resources; - -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; - -public class PermissionCollectionResource { - @GET - @Path("") - public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - throw new UnsupportedOperationException(); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java index ebd49423ab..b184bc3934 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -5,16 +5,25 @@ import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; import lombok.Setter; +import lombok.ToString; -@Getter @Setter +@Getter @Setter @ToString public class PermissionDto extends HalRepresentation { - @JsonInclude(JsonInclude.Include.NON_NULL) - private PermissionTypeDto type = PermissionTypeDto.READ; - @JsonInclude(JsonInclude.Include.NON_NULL) private String name; + /** + * the type can be replaced with a dto enum if the mapstruct 1.3.0 is stable + * the mapstruct has a Bug on mapping enums in the 1.2.0-Final Version + * + * see the bug fix: https://github.com/mapstruct/mapstruct/commit/460e87eef6eb71245b387fdb0509c726676a8e19 + * + **/ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String type ; + + private boolean groupPermission = false; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java new file mode 100644 index 0000000000..9128479836 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -0,0 +1,21 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Permission; + +@Mapper +public abstract class PermissionDtoToPermissionMapper { + + public abstract Permission map(PermissionDto permissionDto); + + /** + * this method is needed to modify an existing permission object + * + * @param target the target permission + * @param permissionDto the source dto + * @return the mapped target permission object + */ + public abstract Permission map(@MappingTarget Permission target, PermissionDto permissionDto); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java new file mode 100644 index 0000000000..42e341ce0d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + 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.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class PermissionNotFoundExceptionMapper extends StatusExceptionMapper { + + public PermissionNotFoundExceptionMapper() { + super(PermissionNotFoundException.class, Response.Status.NOT_FOUND); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index cd1e970e43..e9e1d38d95 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -1,20 +1,234 @@ package sonia.scm.api.v2.resources; -import javax.inject.Inject; -import javax.inject.Provider; -import javax.ws.rs.Path; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import sonia.scm.repository.*; +import sonia.scm.web.VndMediaType; +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j public class PermissionRootResource { - private final Provider permissionCollectionResource; + private PermissionDtoToPermissionMapper dtoToModelMapper; + private PermissionToPermissionDtoMapper modelToDtoMapper; + private ResourceLinks resourceLinks; + private final RepositoryManager manager; + @Inject - public PermissionRootResource(Provider permissionCollectionResource) { - this.permissionCollectionResource = permissionCollectionResource; + public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { + this.dtoToModelMapper = dtoToModelMapper; + this.modelToDtoMapper = modelToDtoMapper; + this.resourceLinks = resourceLinks; + this.manager = manager; } + + /** + * Adds a new permission to the user or group managed by the repository + * + * @param permission permission to add + * @return a web response with the status code 201 and the url to GET the added permission + */ + @POST + @StatusCodes({ + @ResponseCode(code = 201, condition = "creates", additionalHeaders = { + @ResponseHeader(name = "Location", description = "uri of the created permission") + }), + @ResponseCode(code = 500, condition = "internal server error"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 409, condition = "conflict") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PERMISSION) + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException { + log.info("try to add new permission: {}", permission); + Repository repository = checkPermission(namespace, name); + checkPermissionAlreadyExists(permission, repository); + repository.getPermissions().add(dtoToModelMapper.map(permission)); + manager.modify(repository); + return Response.created(URI.create(resourceLinks.permission().self(namespace,name,permission.getName()))).build(); + } + + + /** + * Get the searched permission with permission name related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) + @Path("{permission-name}") + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { + Repository repository = checkPermission(namespace, name); + return Response.ok( + repository.getPermissions() + .stream() + .filter(permission -> StringUtils.isNotBlank(permission.getName()) && permission.getName().equals(permissionName)) + .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .findFirst() + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) + ).build(); + } + + + /** + * Get all permissions related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) @Path("") - public PermissionCollectionResource getPermissionCollectionResource() { - return permissionCollectionResource.get(); + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + Repository repository = checkPermission(namespace, name); + List permissionDtoList = repository.getPermissions() + .stream() + .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .collect(Collectors.toList()); + return Response.ok(permissionDtoList).build(); + } + + + /** + * Update a permission to the user or group managed by the repository + * + * @param permission permission to modify + * @param permissionName permission to modify + * @return a web response with the status code 204 + */ + @PUT + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PERMISSION) + @Path("{permission-name}") + public Response update(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName, + PermissionDto permission) throws RepositoryException { + log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); + Repository repository = checkPermission(namespace, name); + repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .map(p -> dtoToModelMapper.map(p, permission)) + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) + ; + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + /** + * Update a permission to the user or group managed by the repository + * + * @param permissionName permission to delete + * @return a web response with the status code 204 + */ + @DELETE + @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"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Path("{permission-name}") + public Response delete(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName) throws RepositoryException { + log.info("try to delete the permission with name: {}.", permissionName); + Repository repository = checkPermission(namespace, name); + repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .ifPresent(p -> repository.getPermissions().remove(p)) + ; + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + + + /** + * check if the actual user is permitted to manage the repository permissions + * return the repository if the user is permitted + * + * @param namespace the repository namespace + * @param name the repository name + * @return the repository if the user is permitted + * @throws RepositoryNotFoundException if the repository does not exists + */ + private Repository checkPermission(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) + .filter(repository -> { + checkUserPermitted(repository); + return true; + }) + .orElseThrow(() -> new RepositoryNotFoundException(name)); + } + + + /** + * throw exception if the user is not permitted + * @param repository + */ + protected void checkUserPermitted(Repository repository) { + RepositoryPermissions.modify(repository).check(); + } + + + /** + * check if the permission already exists in the repository + * + * @param permission the searched permission + * @param repository the repository to be inspected + * @throws PermissionAlreadyExistsException if the permission already exists in the repository + */ + private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws PermissionAlreadyExistsException { + boolean isPermissionAlreadyExist = repository.getPermissions() + .stream() + .anyMatch(p -> p.getName().equals(permission.getName())); + if (isPermissionAlreadyExist) { + throw new PermissionAlreadyExistsException(repository, permission.getName()); + } } } + + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java new file mode 100644 index 0000000000..49adb0e1c0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java @@ -0,0 +1,37 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.*; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class PermissionToPermissionDtoMapper { + + @Inject + private ResourceLinks resourceLinks; + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract PermissionDto map(Permission permission, @Context NamespaceAndName namespaceAndName); + + /** + * Add the self, update and delete links. + * + * @param target the mapped dto + * @param namespaceAndName the repository namespace and name + */ + @AfterMapping + void appendLinks(@MappingTarget PermissionDto target, @Context NamespaceAndName namespaceAndName) { + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.permission().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())); + linksBuilder.single(link("update", resourceLinks.permission().update(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); + linksBuilder.single(link("delete", resourceLinks.permission().delete(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); + target.add(linksBuilder.build()); + } +} + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java deleted file mode 100644 index 7b280f9f1f..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package sonia.scm.api.v2.resources; - -/** - * Type of permissionPrefix for a {@link RepositoryDto}. - * - * @author mkarray - */ - -public enum PermissionTypeDto { - - /** - * read permission - */ - READ, - - /** - * read and write permission - */ - WRITE, - - /** - * read, write and manage the properties and permissions - */ - OWNER - -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java new file mode 100644 index 0000000000..2116b8e31c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + 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.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.RepositoryNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class RepositoryNotFoundExceptionMapper extends StatusExceptionMapper { + + public RepositoryNotFoundExceptionMapper() { + super(RepositoryNotFoundException.class, Response.Status.NOT_FOUND); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index f46bc22cdf..0263b81048 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -36,7 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper TEST_PERMISSIONS = Lists + .newArrayList( + new Permission("user_write", PermissionType.WRITE, false), + new Permission("user_read", PermissionType.READ, false), + new Permission("user_owner", PermissionType.OWNER, false), + new Permission("group_read", PermissionType.READ, true), + new Permission("group_write", PermissionType.WRITE, true), + new Permission("group_owner", PermissionType.OWNER, true) + ); + private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest() + .description("GET all permissions") + .method("GET") + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestPOSTPermission = new ExpectedRequest() + .description("create new permission") + .method("POST") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestGETPermission = new ExpectedRequest() + .description("GET permission") + .method("GET") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestDELETEPermission = new ExpectedRequest() + .description("delete permission") + .method("DELETE") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestPUTPermission = new ExpectedRequest() + .description("update permission") + .method("PUT") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ONE_PERMISSION); + + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Mock + private RepositoryManager repositoryManager; + + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + private PermissionToPermissionDtoMapperImpl permissionToPermissionDtoMapper; + + @InjectMocks + private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; + + + private PermissionRootResource permissionRootResource; + + + @BeforeEach + @Before + public void prepareEnvironment() { + initMocks(this); + permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider + .of(new RepositoryResource(null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionAlreadyExistsExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + } + + + @Test + public void shouldGetAllPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + assertExpectedRequest(requestGETAllPermissions + .expectedResponseStatus(200) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + List actualPermissionDtos = mapper.readValue(body, new TypeReference>() { + }); + assertThat(actualPermissionDtos) + .as("response payload match permission object models") + .hasSize(TEST_PERMISSIONS.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(getExpectedPermissionDtos(TEST_PERMISSIONS)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + @Test + public void shouldGetPermissionByName() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission expectedPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestGETPermission + .expectedResponseStatus(200) + .path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName()) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class); + assertThat(actualPermissionDto) + .as("response payload match permission object model") + .isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + @Test + public void shouldGetCreatedPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); + ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); + permissions.add(newPermission); + ImmutableList expectedPermissions = ImmutableList.copyOf(permissions); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(201) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("POST response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + @Test + public void shouldNotAddExistingPermission() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission newPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(409) + ); + } + + + @Test + public void shouldGetUpdatedPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission modifiedPermission = TEST_PERMISSIONS.get(0); + // modify the type to owner + modifiedPermission.setType(PermissionType.OWNER); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); + assertExpectedRequest(requestPUTPermission + .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}") + .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("PUT response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + + @Test + public void shouldDeletePermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + @Test + public void deletingNotExistingPermissionShouldProcess() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + private void assertGettingExpectedPermissions(ImmutableList expectedPermissions) { + assertExpectedRequest(requestGETAllPermissions + .expectedResponseStatus(200) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + List actualPermissionDtos = mapper.readValue(body, new TypeReference>() { + }); + assertThat(actualPermissionDtos) + .as("response payload match permission object models") + .hasSize(expectedPermissions.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(getExpectedPermissionDtos(Lists.newArrayList(expectedPermissions))) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + private PermissionDto[] getExpectedPermissionDtos(ArrayList permissions) { + return permissions + .stream() + .map(this::getExpectedPermissionDto) + .toArray(PermissionDto[]::new); + } + + private PermissionDto getExpectedPermissionDto(Permission permission) { + PermissionDto result = new PermissionDto(); + result.setName(permission.getName()); + result.setGroupPermission(permission.isGroupPermission()); + result.setType(permission.getType().name()); + String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permission.getName(); + result.add(linkingTo() + .self(permissionHref) + .single(link("update", permissionHref)) + .single(link("delete", permissionHref)) + .build()); + return result; + } + + @TestFactory + @DisplayName("test endpoints on missing repository and user is Admin") + Stream missedRepositoryTestFactory() { + return createDynamicTestsToAssertResponses( + requestGETAllPermissions.expectedResponseStatus(404), + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(404), + requestDELETEPermission.expectedResponseStatus(404), + requestPUTPermission.expectedResponseStatus(404)); + } + + + @TestFactory + @DisplayName("test endpoints on missing permission and user is Admin") + Stream missedPermissionTestFactory() { + authorizedUserHasARepository(); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(201), + requestGETAllPermissions.expectedResponseStatus(200), + requestDELETEPermission.expectedResponseStatus(204), + requestPUTPermission.expectedResponseStatus(404)); + } + + private Repository authorizedUserHasARepository() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + doNothing().when(permissionRootResource).checkUserPermitted(mockRepository); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return mockRepository; + } + + private void authorizedUserHasARepositoryWithPermissions(ArrayList permissions) { + when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions); + } + + + @TestFactory + @DisplayName("test endpoints on missing permission and user is not Admin") + Stream missedPermissionUserForbiddenTestFactory() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(401), + requestPOSTPermission.expectedResponseStatus(401), + requestGETAllPermissions.expectedResponseStatus(401), + requestDELETEPermission.expectedResponseStatus(401), + requestPUTPermission.expectedResponseStatus(401)); + } + + + private Stream createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) { + + return Stream.of(expectedRequests) + .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); + } + + private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) { + MockHttpResponse response = new MockHttpResponse(); + HttpRequest request = null; + try { + request = MockHttpRequest + .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) + .content(entry.content) + .contentType(VndMediaType.PERMISSION); + } catch (URISyntaxException e) { + fail(e.getMessage()); + } + dispatcher.invoke(request, response); + log.info("Test the Request :{}", entry); + assertThat(entry.expectedResponseStatus) + .as("assert status code") + .isEqualTo(response.getStatus()); + if (entry.responseValidator != null) { + entry.responseValidator.accept(response); + } + return response; + } + + @ToString + public class ExpectedRequest { + private String description; + private String method; + private String path; + private int expectedResponseStatus; + private byte[] content = new byte[]{}; + private Consumer responseValidator; + + public ExpectedRequest description(String description) { + this.description = description; + return this; + } + + public ExpectedRequest method(String method) { + this.method = method; + return this; + } + + public ExpectedRequest path(String path) { + this.path = path; + return this; + } + + public ExpectedRequest content(String content) { + if (content != null) { + this.content = content.getBytes(); + } + return this; + } + + public ExpectedRequest expectedResponseStatus(int expectedResponseStatus) { + this.expectedResponseStatus = expectedResponseStatus; + return this; + } + + public ExpectedRequest responseValidator(Consumer responseValidator) { + this.responseValidator = responseValidator; + return this; + } + } + +} 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 882d754329..6df5ac1a7a 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 @@ -23,7 +23,7 @@ public class ResourceLinksMock { when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); - when(resourceLinks.permissionCollection()).thenReturn(new ResourceLinks.PermissionCollectionLinks(uriInfo)); + when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); From a7ba473fcc0053bcff6f557929ed288164e8dfed Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Tue, 21 Aug 2018 10:58:47 +0200 Subject: [PATCH 3/7] #8771 use the AuthenticationExceptionMapper from v1 --- .../AuthorizationExceptionMapper.java | 50 ------------------- .../resources/PermissionRootResourceTest.java | 11 ++-- 2 files changed, 6 insertions(+), 55 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java deleted file mode 100644 index bf00bbfc5e..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - 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.api.v2.resources; - - -import org.apache.shiro.authz.AuthorizationException; -import sonia.scm.api.rest.StatusExceptionMapper; - -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.Provider; - -/** - * @author mkarray - * @since 2.0.0 - */ -@Provider -public class AuthorizationExceptionMapper extends StatusExceptionMapper { - - public AuthorizationExceptionMapper() { - super(AuthorizationException.class, Response.Status.UNAUTHORIZED); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 23cd51c9f9..7236a98151 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -25,6 +25,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.repository.*; import sonia.scm.web.VndMediaType; @@ -155,11 +156,11 @@ public class PermissionRootResourceTest { doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); return createDynamicTestsToAssertResponses( - requestGETPermission.expectedResponseStatus(401), - requestPOSTPermission.expectedResponseStatus(401), - requestGETAllPermissions.expectedResponseStatus(401), - requestDELETEPermission.expectedResponseStatus(401), - requestPUTPermission.expectedResponseStatus(401)); + requestGETPermission.expectedResponseStatus(403), + requestPOSTPermission.expectedResponseStatus(403), + requestGETAllPermissions.expectedResponseStatus(403), + requestDELETEPermission.expectedResponseStatus(403), + requestPUTPermission.expectedResponseStatus(403)); } @Test From 778881a2fbe1c4f813d809bf69a91030b6453f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 21 Aug 2018 11:47:02 +0200 Subject: [PATCH 4/7] Peer-Review --- .../PermissionAlreadyExistsException.java | 2 +- .../AuthorizationExceptionMapper.java | 1 - ...ermissionAlreadyExistsExceptionMapper.java | 1 - .../PermissionNotFoundExceptionMapper.java | 1 - .../v2/resources/PermissionRootResource.java | 66 +++++++++++-------- .../RepositoryNotFoundExceptionMapper.java | 1 - .../resources/PermissionRootResourceTest.java | 53 +++++++-------- 7 files changed, 64 insertions(+), 61 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java index aeaf64a3e9..43ad0a5e1d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java @@ -5,7 +5,7 @@ import java.text.MessageFormat; public class PermissionAlreadyExistsException extends RepositoryException { public PermissionAlreadyExistsException(Repository repository, String permissionName) { - super(MessageFormat.format("the permission {0} of the repository {1}/{2} is already exists", permissionName, repository.getNamespace(), repository.getName())); + super(MessageFormat.format("the permission {0} of the repository {1}/{2} already exists", permissionName, repository.getNamespace(), repository.getName())); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java index bf00bbfc5e..1e120df966 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java index 0cf83f097a..d654f8bca7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java index 42e341ce0d..61d62ecac5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index e9e1d38d95..716e240bee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -6,11 +6,25 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; -import sonia.scm.repository.*; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.PermissionAlreadyExistsException; +import sonia.scm.repository.PermissionNotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +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; import java.net.URI; import java.util.List; @@ -58,7 +72,7 @@ public class PermissionRootResource { checkPermissionAlreadyExists(permission, repository); repository.getPermissions().add(dtoToModelMapper.map(permission)); manager.modify(repository); - return Response.created(URI.create(resourceLinks.permission().self(namespace,name,permission.getName()))).build(); + return Response.created(URI.create(resourceLinks.permission().self(namespace, name, permission.getName()))).build(); } @@ -84,8 +98,8 @@ public class PermissionRootResource { return Response.ok( repository.getPermissions() .stream() - .filter(permission -> StringUtils.isNotBlank(permission.getName()) && permission.getName().equals(permissionName)) - .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .filter(permission -> permissionName.equals(permission.getName())) + .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(), repository.getName()))) .findFirst() .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) ).build(); @@ -113,7 +127,7 @@ public class PermissionRootResource { Repository repository = checkPermission(namespace, name); List permissionDtoList = repository.getPermissions() .stream() - .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(), repository.getName()))) .collect(Collectors.toList()); return Response.ok(permissionDtoList).build(); } @@ -136,56 +150,55 @@ public class PermissionRootResource { @Consumes(VndMediaType.PERMISSION) @Path("{permission-name}") public Response update(@PathParam("namespace") String namespace, - @PathParam("name") String name, - @PathParam("permission-name") String permissionName, - PermissionDto permission) throws RepositoryException { + @PathParam("name") String name, + @PathParam("permission-name") String permissionName, + PermissionDto permission) throws RepositoryException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = checkPermission(namespace, name); repository.getPermissions() .stream() - .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) .findFirst() - .map(p -> dtoToModelMapper.map(p, permission)) + .map(p -> dtoToModelMapper.map(p, permission)) .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) - ; + ; manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); return Response.noContent().build(); } - /** + /** * Update a permission to the user or group managed by the repository * * @param permissionName permission to delete * @return a web response with the status code 204 */ - @DELETE - @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"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @DELETE + @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"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) @Path("{permission-name}") public Response delete(@PathParam("namespace") String namespace, - @PathParam("name") String name, - @PathParam("permission-name") String permissionName) throws RepositoryException { + @PathParam("name") String name, + @PathParam("permission-name") String permissionName) throws RepositoryException { log.info("try to delete the permission with name: {}.", permissionName); Repository repository = checkPermission(namespace, name); repository.getPermissions() .stream() - .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) .findFirst() .ifPresent(p -> repository.getPermissions().remove(p)) - ; + ; manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); return Response.noContent().build(); } - /** * check if the actual user is permitted to manage the repository permissions * return the repository if the user is permitted @@ -207,6 +220,7 @@ public class PermissionRootResource { /** * throw exception if the user is not permitted + * * @param repository */ protected void checkUserPermitted(Repository repository) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java index 2116b8e31c..dcab8e4fc0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 23cd51c9f9..bf78e7fe68 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -2,8 +2,6 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.core.type.TypeReference; 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 lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -15,7 +13,6 @@ import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.spi.HttpRequest; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -25,7 +22,11 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.repository.*; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; import java.io.IOException; @@ -42,14 +43,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; -@SubjectAware( - username = "trillian", - password = "secret", - configuration = "classpath:sonia/scm/repository/shiro.ini" -) @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j public class PermissionRootResourceTest { @@ -93,9 +93,6 @@ public class PermissionRootResourceTest { private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Rule - public ShiroRule shiro = new ShiroRule(); - @Mock private RepositoryManager repositoryManager; @@ -163,13 +160,13 @@ public class PermissionRootResourceTest { } @Test - public void shouldGetAllPermissions() { + public void shouldGetAllPermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS)); } @Test - public void shouldGetPermissionByName() { + public void shouldGetPermissionByName() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission expectedPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestGETPermission @@ -192,7 +189,7 @@ public class PermissionRootResourceTest { } @Test - public void shouldGetCreatedPermissions() { + public void shouldGetCreatedPermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); @@ -209,7 +206,7 @@ public class PermissionRootResourceTest { } @Test - public void shouldNotAddExistingPermission() { + public void shouldNotAddExistingPermission() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission newPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestPOSTPermission @@ -219,7 +216,7 @@ public class PermissionRootResourceTest { } @Test - public void shouldGetUpdatedPermissions() { + public void shouldGetUpdatedPermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission modifiedPermission = TEST_PERMISSIONS.get(0); // modify the type to owner @@ -238,7 +235,7 @@ public class PermissionRootResourceTest { @Test - public void shouldDeletePermissions() { + public void shouldDeletePermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission deletedPermission = TEST_PERMISSIONS.get(0); ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); @@ -253,7 +250,7 @@ public class PermissionRootResourceTest { } @Test - public void deletingNotExistingPermissionShouldProcess() { + public void deletingNotExistingPermissionShouldProcess() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission deletedPermission = TEST_PERMISSIONS.get(0); ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); @@ -275,7 +272,7 @@ public class PermissionRootResourceTest { assertGettingExpectedPermissions(expectedPermissions); } - private void assertGettingExpectedPermissions(ImmutableList expectedPermissions) { + private void assertGettingExpectedPermissions(ImmutableList expectedPermissions) throws URISyntaxException { assertExpectedRequest(requestGETAllPermissions .expectedResponseStatus(200) .responseValidator((response) -> { @@ -337,17 +334,13 @@ public class PermissionRootResourceTest { .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); } - private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) { + private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) throws URISyntaxException { MockHttpResponse response = new MockHttpResponse(); HttpRequest request = null; - try { - request = MockHttpRequest - .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) - .content(entry.content) - .contentType(VndMediaType.PERMISSION); - } catch (URISyntaxException e) { - fail(e.getMessage()); - } + request = MockHttpRequest + .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) + .content(entry.content) + .contentType(VndMediaType.PERMISSION); dispatcher.invoke(request, response); log.info("Test the Request :{}", entry); assertThat(entry.expectedResponseStatus) From cb59226023e97721cfedf42095d055e3aa438ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 21 Aug 2018 09:57:36 +0000 Subject: [PATCH 5/7] Close branch feature/permission_endpoint_v2 From 298430a90fa706d19eae329348547a0ac0b46857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 21 Aug 2018 13:38:37 +0200 Subject: [PATCH 6/7] Satisfy sonar --- .../api/v2/resources/PermissionDtoToPermissionMapper.java | 2 +- .../scm/api/v2/resources/PermissionRootResource.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java index 9128479836..29cb7e496a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -16,6 +16,6 @@ public abstract class PermissionDtoToPermissionMapper { * @param permissionDto the source dto * @return the mapped target permission object */ - public abstract Permission map(@MappingTarget Permission target, PermissionDto permissionDto); + public abstract Permission modify(@MappingTarget Permission target, PermissionDto permissionDto); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 716e240bee..fdce46b727 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -7,6 +7,7 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; import sonia.scm.repository.PermissionAlreadyExistsException; import sonia.scm.repository.PermissionNotFoundException; import sonia.scm.repository.Repository; @@ -155,13 +156,12 @@ public class PermissionRootResource { PermissionDto permission) throws RepositoryException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = checkPermission(namespace, name); - repository.getPermissions() + Permission existingPermission = repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) .findFirst() - .map(p -> dtoToModelMapper.map(p, permission)) - .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) - ; + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)); + dtoToModelMapper.modify(existingPermission, permission); manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); return Response.noContent().build(); From cd54086e7da755f29ff6ebd4405b782b1fceba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 21 Aug 2018 14:18:03 +0200 Subject: [PATCH 7/7] Remove redundant permission check --- .../PermissionDtoToPermissionMapper.java | 2 +- .../v2/resources/PermissionRootResource.java | 28 ++++--------------- .../resources/PermissionRootResourceTest.java | 15 +++------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java index 29cb7e496a..1e90c23aa7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -16,6 +16,6 @@ public abstract class PermissionDtoToPermissionMapper { * @param permissionDto the source dto * @return the mapped target permission object */ - public abstract Permission modify(@MappingTarget Permission target, PermissionDto permissionDto); + public abstract void modify(@MappingTarget Permission target, PermissionDto permissionDto); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index fdce46b727..d08f2e7057 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -14,7 +14,6 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -69,7 +68,7 @@ public class PermissionRootResource { @Consumes(VndMediaType.PERMISSION) public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException { log.info("try to add new permission: {}", permission); - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); checkPermissionAlreadyExists(permission, repository); repository.getPermissions().add(dtoToModelMapper.map(permission)); manager.modify(repository); @@ -95,7 +94,7 @@ public class PermissionRootResource { @TypeHint(PermissionDto.class) @Path("{permission-name}") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); return Response.ok( repository.getPermissions() .stream() @@ -125,7 +124,7 @@ public class PermissionRootResource { @TypeHint(PermissionDto.class) @Path("") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); List permissionDtoList = repository.getPermissions() .stream() .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(), repository.getName()))) @@ -155,7 +154,7 @@ public class PermissionRootResource { @PathParam("permission-name") String permissionName, PermissionDto permission) throws RepositoryException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); Permission existingPermission = repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) @@ -186,7 +185,7 @@ public class PermissionRootResource { @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { log.info("try to delete the permission with name: {}.", permissionName); - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) @@ -208,26 +207,11 @@ public class PermissionRootResource { * @return the repository if the user is permitted * @throws RepositoryNotFoundException if the repository does not exists */ - private Repository checkPermission(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + private Repository load(String namespace, String name) throws RepositoryNotFoundException { return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) - .filter(repository -> { - checkUserPermitted(repository); - return true; - }) .orElseThrow(() -> new RepositoryNotFoundException(name)); } - - /** - * throw exception if the user is not permitted - * - * @param repository - */ - protected void checkUserPermitted(Repository repository) { - RepositoryPermissions.modify(repository).check(); - } - - /** * check if the permission already exists in the repository * diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 547e2dbcb1..39da0ce42e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -18,10 +18,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Permission; @@ -44,14 +42,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; -@RunWith(MockitoJUnitRunner.Silent.class) @Slf4j public class PermissionRootResourceTest { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; @@ -112,7 +107,7 @@ public class PermissionRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); + permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider .of(new RepositoryResource(null, null, null, null, null, null, null,null, MockProvider.of(permissionRootResource))), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); @@ -150,8 +145,7 @@ public class PermissionRootResourceTest { Stream missedPermissionUserForbiddenTestFactory() { Repository mockRepository = mock(Repository.class); when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); - doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); - when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); return createDynamicTestsToAssertResponses( requestGETPermission.expectedResponseStatus(403), requestPOSTPermission.expectedResponseStatus(403), @@ -321,7 +315,6 @@ public class PermissionRootResourceTest { when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); - doNothing().when(permissionRootResource).checkUserPermitted(mockRepository); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); return mockRepository; } @@ -344,9 +337,9 @@ public class PermissionRootResourceTest { .contentType(VndMediaType.PERMISSION); dispatcher.invoke(request, response); log.info("Test the Request :{}", entry); - assertThat(entry.expectedResponseStatus) + assertThat(response.getStatus()) .as("assert status code") - .isEqualTo(response.getStatus()); + .isEqualTo(entry.expectedResponseStatus); if (entry.responseValidator != null) { entry.responseValidator.accept(response); }