mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Merged in feature/custom_roles_overview (pull request #250)
custom roles overview
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<RepositoryPermission> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> verbs;
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* This constructor exists for mapstruct and JAXB, only -- <b>do not use this in "normal" code</b>.
|
||||
@@ -87,6 +89,15 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
{
|
||||
this.name = name;
|
||||
this.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 == null? -1: verbs.size(), role, groupPermission);
|
||||
}
|
||||
|
||||
|
||||
@@ -142,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();
|
||||
@@ -173,6 +186,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 +215,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 +232,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 +244,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 +273,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));
|
||||
}
|
||||
}
|
||||
|
||||
227
scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java
Normal file
227
scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
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.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 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.
|
||||
*/
|
||||
@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"read", "modify"})
|
||||
@XmlRootElement(name = "roles")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class RepositoryRole implements ModelObject, PermissionObject {
|
||||
|
||||
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<String> verbs;
|
||||
|
||||
private Long creationDate;
|
||||
private Long lastModified;
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* This constructor exists for mapstruct and JAXB, only -- <b>do not use this in "normal" code</b>.
|
||||
*
|
||||
* @deprecated Do not use this for "normal" code.
|
||||
* Use {@link RepositoryRole#RepositoryRole(String, Collection, String)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public RepositoryRole() {}
|
||||
|
||||
public RepositoryRole(String name, Collection<String> 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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.GenericDAO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface RepositoryRoleDAO extends GenericDAO<RepositoryRole> {
|
||||
@Override
|
||||
List<RepositoryRole> getAll();
|
||||
}
|
||||
@@ -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<RepositoryRole> {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -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<RepositoryRole> {
|
||||
}
|
||||
@@ -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<RepositoryRole>
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Repository> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
public class XmlRepositoryRoleDAO extends AbstractXmlDAO<RepositoryRole, XmlRepositoryRoleDatabase>
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RepositoryRole> getAll() {
|
||||
return (List<RepositoryRole>) super.getAll();
|
||||
}
|
||||
}
|
||||
@@ -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<RepositoryRole> {
|
||||
|
||||
private Long creationTime;
|
||||
private Long lastModified;
|
||||
|
||||
@XmlJavaTypeAdapter(XmlRepositoryRoleMapAdapter.class)
|
||||
@XmlElement(name = "roles")
|
||||
private Map<String, RepositoryRole> 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<RepositoryRole> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<RepositoryRole> {
|
||||
|
||||
public XmlRepositoryRoleList() {}
|
||||
|
||||
public XmlRepositoryRoleList(Map<String, RepositoryRole> roleMap) {
|
||||
this.roles = new LinkedList<RepositoryRole>(roleMap.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<RepositoryRole> iterator()
|
||||
{
|
||||
return roles.iterator();
|
||||
}
|
||||
|
||||
public LinkedList<RepositoryRole> getRoles()
|
||||
{
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(LinkedList<RepositoryRole> roles)
|
||||
{
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
@XmlElement(name = "role")
|
||||
private LinkedList<RepositoryRole> roles;
|
||||
}
|
||||
@@ -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<XmlRepositoryRoleList, Map<String, RepositoryRole>> {
|
||||
|
||||
@Override
|
||||
public XmlRepositoryRoleList marshal(Map<String, RepositoryRole> roleMap) {
|
||||
return new XmlRepositoryRoleList(roleMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, RepositoryRole> unmarshal(XmlRepositoryRoleList roles) {
|
||||
Map<String, RepositoryRole> roleMap = new LinkedHashMap<>();
|
||||
|
||||
for (RepositoryRole role : roles) {
|
||||
roleMap.put(role.getName(), role);
|
||||
}
|
||||
|
||||
return roleMap;
|
||||
}
|
||||
}
|
||||
77
scm-it/src/test/java/sonia/scm/it/RoleITCase.java
Normal file
77
scm-it/src/test/java/sonia/scm/it/RoleITCase.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
String repositoryRolesUrl = new ScmRequests()
|
||||
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.getUrl("repositoryRoles");
|
||||
|
||||
given()
|
||||
.when()
|
||||
.delete(repositoryRolesUrl + ROLE_NAME)
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_NO_CONTENT);
|
||||
|
||||
given(VndMediaType.REPOSITORY_ROLE)
|
||||
.when()
|
||||
.content("{" +
|
||||
"\"name\": \"" + ROLE_NAME + "\"," +
|
||||
"\"verbs\": [\"read\",\"permissionRead\"]" +
|
||||
"}")
|
||||
.post(repositoryRolesUrl)
|
||||
.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"));
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -106,14 +106,31 @@ public class TestData {
|
||||
;
|
||||
}
|
||||
|
||||
public static void createUserPermission(String name, Collection<String> permissionType, String repositoryType) {
|
||||
public static void createUserPermission(String username, Collection<String> 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" +
|
||||
"}")
|
||||
|
||||
@@ -12,6 +12,8 @@ export type ButtonProps = {
|
||||
fullWidth?: boolean,
|
||||
className?: string,
|
||||
children?: React.Node,
|
||||
|
||||
// context props
|
||||
classes: any
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export type RepositoryRole = {
|
||||
name: string,
|
||||
verbs: string[]
|
||||
};
|
||||
|
||||
export type AvailableRepositoryPermissions = {
|
||||
availableVerbs: string[],
|
||||
availableRoles: RepositoryRole[]
|
||||
};
|
||||
@@ -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[];
|
||||
|
||||
13
scm-ui-components/packages/ui-types/src/RepositoryRole.js
Normal file
13
scm-ui-components/packages/ui-types/src/RepositoryRole.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import type {Links} from "./hal";
|
||||
|
||||
export type RepositoryRole = {
|
||||
name: string,
|
||||
verbs: string[],
|
||||
type?: string,
|
||||
creationDate?: string,
|
||||
lastModified?: string,
|
||||
_links: Links
|
||||
};
|
||||
|
||||
@@ -25,6 +25,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";
|
||||
|
||||
@@ -6,6 +6,33 @@
|
||||
"errorTitle": "Fehler",
|
||||
"errorSubtitle": "Unbekannter Einstellungen Fehler"
|
||||
},
|
||||
"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",
|
||||
"permissions": "Berechtigungen",
|
||||
"submit": "Speichern"
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"name": "Name",
|
||||
"system": "System"
|
||||
},
|
||||
"config-form": {
|
||||
"submit": "Speichern",
|
||||
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6,6 +6,33 @@
|
||||
"errorTitle": "Error",
|
||||
"errorSubtitle": "Unknown Config Error"
|
||||
},
|
||||
"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",
|
||||
"permissions": "Permissions",
|
||||
"submit": "Save"
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"name": "Name",
|
||||
"system": "System"
|
||||
},
|
||||
"config-form": {
|
||||
"submit": "Submit",
|
||||
"submit-success-notification": "Configuration changed successfully!",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// @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 { Links } from "@scm-manager/ui-types";
|
||||
import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components";
|
||||
import GlobalConfig from "./GlobalConfig";
|
||||
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 RepositoryRoles from "../roles/containers/RepositoryRoles";
|
||||
import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole";
|
||||
import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole";
|
||||
|
||||
type Props = {
|
||||
links: Links,
|
||||
@@ -33,6 +35,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/`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { links, t } = this.props;
|
||||
|
||||
@@ -46,12 +54,44 @@ 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`}
|
||||
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">
|
||||
<Navigation>
|
||||
@@ -60,6 +100,11 @@ class Config extends React.Component<Props> {
|
||||
to={`${url}`}
|
||||
label={t("config.globalConfigurationNavLink")}
|
||||
/>
|
||||
<NavLink
|
||||
to={`${url}/roles/`}
|
||||
label={t("repositoryRole.navLink")}
|
||||
activeWhenMatch={this.matchesRoles}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="config.navigation"
|
||||
props={extensionProps}
|
||||
|
||||
47
scm-ui/src/config/roles/components/AvailableVerbs.js
Normal file
47
scm-ui/src/config/roles/components/AvailableVerbs.js
Normal file
@@ -0,0 +1,47 @@
|
||||
//@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";
|
||||
|
||||
type Props = {
|
||||
role: RepositoryRole,
|
||||
// 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);
|
||||
52
scm-ui/src/config/roles/components/PermissionRoleDetails.js
Normal file
52
scm-ui/src/config/roles/components/PermissionRoleDetails.js
Normal file
@@ -0,0 +1,52 @@
|
||||
//@flow
|
||||
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 PermissionRoleDetailsTable from "./PermissionRoleDetailsTable";
|
||||
import { Button, Subtitle } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
role: RepositoryRole,
|
||||
url: string,
|
||||
|
||||
// context props
|
||||
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 } = 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("config")(PermissionRoleDetails);
|
||||
@@ -0,0 +1,37 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
import AvailableVerbs from "./AvailableVerbs";
|
||||
|
||||
type Props = {
|
||||
role: RepositoryRole,
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
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>
|
||||
<tr>
|
||||
<th>{t("repositoryRole.verbs")}</th>
|
||||
<AvailableVerbs role={role} />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("config")(PermissionRoleDetailsTable);
|
||||
33
scm-ui/src/config/roles/components/PermissionRoleRow.js
Normal file
33
scm-ui/src/config/roles/components/PermissionRoleRow.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import SystemRoleTag from "./SystemRoleTag";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string,
|
||||
role: RepositoryRole
|
||||
};
|
||||
|
||||
class PermissionRoleRow extends React.Component<Props> {
|
||||
renderLink(to: string, label: string, system?: boolean) {
|
||||
return (
|
||||
<Link to={to}>
|
||||
{label} <SystemRoleTag system={system} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { baseUrl, role } = this.props;
|
||||
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>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PermissionRoleRow;
|
||||
36
scm-ui/src/config/roles/components/PermissionRoleTable.js
Normal file
36
scm-ui/src/config/roles/components/PermissionRoleTable.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import PermissionRoleRow from "./PermissionRoleRow";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string,
|
||||
roles: RepositoryRole[],
|
||||
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class PermissionRoleTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { baseUrl, roles, t } = this.props;
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("repositoryRole.form.name")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{roles.map((role, index) => {
|
||||
return (
|
||||
<PermissionRoleRow key={index} baseUrl={baseUrl} role={role} />
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("config")(PermissionRoleTable);
|
||||
35
scm-ui/src/config/roles/components/SystemRoleTag.js
Normal file
35
scm-ui/src/config/roles/components/SystemRoleTag.js
Normal file
@@ -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("role.system")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(translate("config")(SystemRoleTag));
|
||||
87
scm-ui/src/config/roles/containers/CreateRepositoryRole.js
Normal file
87
scm-ui/src/config/roles/containers/CreateRepositoryRole.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// @flow
|
||||
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 {
|
||||
createRole,
|
||||
getCreateRoleFailure,
|
||||
getFetchVerbsFailure,
|
||||
isFetchVerbsPending
|
||||
} from "../modules/roles";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import {
|
||||
getRepositoryRolesLink,
|
||||
getRepositoryVerbsLink
|
||||
} from "../../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean,
|
||||
repositoryRolesLink: string,
|
||||
error?: Error,
|
||||
|
||||
//dispatch function
|
||||
addRole: (link: string, role: RepositoryRole, callback?: () => void) => void,
|
||||
|
||||
// context objects
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class CreateRepositoryRole extends React.Component<Props> {
|
||||
repositoryRoleCreated = (role: RepositoryRole) => {
|
||||
const { history } = this.props;
|
||||
history.push("/config/role/" + role.name + "/info");
|
||||
};
|
||||
|
||||
createRepositoryRole = (role: RepositoryRole) => {
|
||||
this.props.addRole(this.props.repositoryRolesLink, role, () =>
|
||||
this.repositoryRoleCreated(role)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title={t("repositoryRole.title")} />
|
||||
<RepositoryRoleForm
|
||||
disabled={this.props.disabled}
|
||||
submitForm={role => this.createRepositoryRole(role)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isFetchVerbsPending(state);
|
||||
const error = getFetchVerbsFailure(state) || getCreateRoleFailure(state);
|
||||
const verbsLink = getRepositoryVerbsLink(state);
|
||||
const repositoryRolesLink = getRepositoryRolesLink(state);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
verbsLink,
|
||||
repositoryRolesLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
addRole: (link: string, role: RepositoryRole, callback?: () => void) => {
|
||||
dispatch(createRole(link, role, callback));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("config")(CreateRepositoryRole));
|
||||
78
scm-ui/src/config/roles/containers/EditRepositoryRole.js
Normal file
78
scm-ui/src/config/roles/containers/EditRepositoryRole.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// @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 { ErrorNotification } from "@scm-manager/ui-components";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean,
|
||||
role: RepositoryRole,
|
||||
repositoryRolesLink: string,
|
||||
error?: Error,
|
||||
|
||||
//dispatch function
|
||||
updateRole: (
|
||||
link: string,
|
||||
role: RepositoryRole,
|
||||
callback?: () => void
|
||||
) => void
|
||||
};
|
||||
|
||||
class EditRepositoryRole extends React.Component<Props> {
|
||||
repositoryRoleUpdated = (role: RepositoryRole) => {
|
||||
const { history } = this.props;
|
||||
history.push("/config/roles/");
|
||||
};
|
||||
|
||||
updateRepositoryRole = (role: RepositoryRole) => {
|
||||
this.props.updateRole(role, () => this.repositoryRoleUpdated(role));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RepositoryRoleForm
|
||||
nameDisabled={true}
|
||||
role={this.props.role}
|
||||
submitForm={role => this.updateRepositoryRole(role)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isModifyRolePending(state);
|
||||
const error = getModifyRoleFailure(state, ownProps.role.name);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
updateRole: (role: RepositoryRole, callback?: () => void) => {
|
||||
dispatch(modifyRole(role, callback));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("config")(EditRepositoryRole));
|
||||
176
scm-ui/src/config/roles/containers/RepositoryRoleForm.js
Normal file
176
scm-ui/src/config/roles/containers/RepositoryRoleForm.js
Normal file
@@ -0,0 +1,176 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import { InputField, SubmitButton } from "@scm-manager/ui-components";
|
||||
import PermissionCheckbox from "../../../repos/permissions/components/PermissionCheckbox";
|
||||
import {
|
||||
fetchAvailableVerbs,
|
||||
getFetchVerbsFailure,
|
||||
getVerbsFromState,
|
||||
isFetchVerbsPending
|
||||
} from "../modules/roles";
|
||||
import {
|
||||
getRepositoryRolesLink,
|
||||
getRepositoryVerbsLink
|
||||
} from "../../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
role?: RepositoryRole,
|
||||
loading?: boolean,
|
||||
nameDisabled: boolean,
|
||||
availableVerbs: string[],
|
||||
verbsLink: string,
|
||||
submitForm: RepositoryRole => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
|
||||
// dispatch functions
|
||||
fetchAvailableVerbs: (link: string) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
role: RepositoryRole
|
||||
};
|
||||
|
||||
class RepositoryRoleForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
role: {
|
||||
name: "",
|
||||
verbs: [],
|
||||
system: false,
|
||||
_links: {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { fetchAvailableVerbs, verbsLink } = this.props;
|
||||
fetchAvailableVerbs(verbsLink);
|
||||
if (this.props.role) {
|
||||
this.setState({ role: this.props.role });
|
||||
}
|
||||
}
|
||||
|
||||
isFalsy(value) {
|
||||
return !value;
|
||||
}
|
||||
|
||||
isValid = () => {
|
||||
const { role } = this.state;
|
||||
return !(
|
||||
this.isFalsy(role) ||
|
||||
this.isFalsy(role.name) ||
|
||||
this.isFalsy(role.verbs.length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
handleNameChange = (name: string) => {
|
||||
this.setState({
|
||||
role: {
|
||||
...this.state.role,
|
||||
name
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleVerbChange = (value: boolean, name: string) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, availableVerbs, nameDisabled, t } = this.props;
|
||||
const { role } = this.state;
|
||||
|
||||
const verbSelectBoxes = !availableVerbs
|
||||
? null
|
||||
: availableVerbs.map(verb => (
|
||||
<PermissionCheckbox
|
||||
key={verb}
|
||||
name={verb}
|
||||
checked={role.verbs.includes(verb)}
|
||||
onChange={this.handleVerbChange}
|
||||
/>
|
||||
));
|
||||
|
||||
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={nameDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<>{verbSelectBoxes}</>
|
||||
<hr />
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<SubmitButton
|
||||
loading={loading}
|
||||
label={t("repositoryRole.form.submit")}
|
||||
disabled={!this.isValid()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isFetchVerbsPending(state);
|
||||
const error = getFetchVerbsFailure(state);
|
||||
const verbsLink = getRepositoryVerbsLink(state);
|
||||
const availableVerbs = getVerbsFromState(state);
|
||||
const repositoryRolesLink = getRepositoryRolesLink(state);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
verbsLink,
|
||||
availableVerbs,
|
||||
repositoryRolesLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchAvailableVerbs: (link: string) => {
|
||||
dispatch(fetchAvailableVerbs(link));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("config")(RepositoryRoleForm));
|
||||
152
scm-ui/src/config/roles/containers/RepositoryRoles.js
Normal file
152
scm-ui/src/config/roles/containers/RepositoryRoles.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// @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 { RepositoryRole, PagedCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Title,
|
||||
Loading,
|
||||
Notification,
|
||||
LinkPaginator,
|
||||
urls,
|
||||
CreateButton
|
||||
} from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchRolesByPage,
|
||||
getRolesFromState,
|
||||
selectListAsCollection,
|
||||
isPermittedToCreateRoles,
|
||||
isFetchRolesPending,
|
||||
getFetchRolesFailure
|
||||
} from "../modules/roles";
|
||||
import PermissionRoleTable from "../components/PermissionRoleTable";
|
||||
import { getRolesLink } from "../../../modules/indexResource";
|
||||
type Props = {
|
||||
baseUrl: string,
|
||||
roles: RepositoryRole[],
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
canAddRoles: boolean,
|
||||
list: PagedCollection,
|
||||
page: number,
|
||||
rolesLink: string,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
|
||||
// dispatch functions
|
||||
fetchRolesByPage: (link: string, page: number) => void
|
||||
};
|
||||
|
||||
class RepositoryRoles extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchRolesByPage, rolesLink, page } = this.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;
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title title={t("repositoryRole.title")} />
|
||||
{this.renderPermissionsTable()}
|
||||
{this.renderCreateButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPermissionsTable() {
|
||||
const { baseUrl, roles, list, page, t } = this.props;
|
||||
if (roles && roles.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<PermissionRoleTable baseUrl={baseUrl} roles={roles} />
|
||||
<LinkPaginator collection={list} page={page} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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 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) => {
|
||||
dispatch(fetchRolesByPage(link, page));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("config")(RepositoryRoles))
|
||||
);
|
||||
137
scm-ui/src/config/roles/containers/SingleRepositoryRole.js
Normal file
137
scm-ui/src/config/roles/containers/SingleRepositoryRole.js
Normal file
@@ -0,0 +1,137 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
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 { translate } from "react-i18next";
|
||||
import type { RepositoryRole } 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 { withRouter } from "react-router-dom";
|
||||
import PermissionRoleDetail from "../components/PermissionRoleDetails";
|
||||
import EditRepositoryRole from "./EditRepositoryRole";
|
||||
|
||||
type Props = {
|
||||
roleName: string,
|
||||
role: RepositoryRole,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
repositoryRolesLink: string,
|
||||
disabled: boolean,
|
||||
|
||||
// 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.roleName
|
||||
);
|
||||
}
|
||||
|
||||
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("repositoryRole.errorTitle")}
|
||||
subtitle={t("repositoryRole.errorSubtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!role || loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
const extensionProps = {
|
||||
role,
|
||||
url
|
||||
};
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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,
|
||||
roleName,
|
||||
role,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchRoleByName: (link: string, name: string) => {
|
||||
dispatch(fetchRoleByName(link, name));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("config")(SingleRepositoryRole))
|
||||
);
|
||||
542
scm-ui/src/config/roles/modules/roles.js
Normal file
542
scm-ui/src/config/roles/modules/roles.js
Normal file
@@ -0,0 +1,542 @@
|
||||
// @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,
|
||||
RepositoryRole
|
||||
} 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}`;
|
||||
|
||||
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
|
||||
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) {
|
||||
// backend start counting by 0
|
||||
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: RepositoryRole) {
|
||||
return fetchRole(role._links.self.href, role.name);
|
||||
}
|
||||
|
||||
// create role
|
||||
export function createRolePending(role: RepositoryRole): 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: RepositoryRole,
|
||||
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)));
|
||||
};
|
||||
}
|
||||
|
||||
//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;
|
||||
return { ...state, verbs };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// modify role
|
||||
export function modifyRolePending(role: RepositoryRole): Action {
|
||||
return {
|
||||
type: MODIFY_ROLE_PENDING,
|
||||
payload: role,
|
||||
itemId: role.name
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyRoleSuccess(role: RepositoryRole): Action {
|
||||
return {
|
||||
type: MODIFY_ROLE_SUCCESS,
|
||||
payload: role,
|
||||
itemId: role.name
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyRoleFailure(role: RepositoryRole, error: Error): Action {
|
||||
return {
|
||||
type: MODIFY_ROLE_FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
role
|
||||
},
|
||||
itemId: role.name
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyRoleReset(role: RepositoryRole): Action {
|
||||
return {
|
||||
type: MODIFY_ROLE_RESET,
|
||||
itemId: role.name
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyRole(role: RepositoryRole, 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: RepositoryRole): Action {
|
||||
return {
|
||||
type: DELETE_ROLE_PENDING,
|
||||
payload: role,
|
||||
itemId: role.name
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteRoleSuccess(role: RepositoryRole): Action {
|
||||
return {
|
||||
type: DELETE_ROLE_SUCCESS,
|
||||
payload: role,
|
||||
itemId: role.name
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteRoleFailure(role: RepositoryRole, error: Error): Action {
|
||||
return {
|
||||
type: DELETE_ROLE_FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
role
|
||||
},
|
||||
itemId: role.name
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteRole(role: RepositoryRole, 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: RepositoryRole[],
|
||||
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) => {
|
||||
return {
|
||||
...state,
|
||||
[rolename]: newRoleState
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
case FETCH_ROLES_SUCCESS:
|
||||
const roles = action.payload._embedded.repositoryRoles;
|
||||
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,
|
||||
verbs: verbReducer
|
||||
});
|
||||
|
||||
// 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 => {
|
||||
return !!selectListEntry(state).roleCreatePermission;
|
||||
};
|
||||
|
||||
export function getRolesFromState(state: Object) {
|
||||
const roleNames = selectList(state).entries;
|
||||
if (!roleNames) {
|
||||
return null;
|
||||
}
|
||||
const roleEntries: RepositoryRole[] = [];
|
||||
|
||||
for (let roleName of roleNames) {
|
||||
roleEntries.push(state.roles.byNames[roleName]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function isFetchRolesPending(state: Object) {
|
||||
return isPending(state, FETCH_ROLES);
|
||||
}
|
||||
|
||||
export function getFetchRolesFailure(state: Object) {
|
||||
return getFailure(state, FETCH_ROLES);
|
||||
}
|
||||
|
||||
export function isFetchVerbsPending(state: Object) {
|
||||
return isPending(state, FETCH_VERBS);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
653
scm-ui/src/config/roles/modules/roles.test.js
Normal file
653
scm-ui/src/config/roles/modules/roles.test.js
Normal file
@@ -0,0 +1,653 @@
|
||||
// @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 role1 = {
|
||||
name: "specialrole",
|
||||
verbs: ["read", "pull", "push", "readPullRequest"],
|
||||
system: false,
|
||||
_links: {
|
||||
self: {
|
||||
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 = {
|
||||
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 = {
|
||||
headers: { "content-type": "application/json" },
|
||||
responseBody
|
||||
};
|
||||
|
||||
const URL = "repositoryRoles";
|
||||
const ROLES_URL = "/api/v2/repositoryRoles";
|
||||
const ROLE1_URL =
|
||||
"http://localhost:8081/scm/api/v2/repositoryRoles/specialrole";
|
||||
|
||||
const error = new Error("FEHLER!");
|
||||
|
||||
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 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, {
|
||||
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", () => {
|
||||
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("repository roles selector", () => {
|
||||
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 repositoryRoles 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 repositoryRoles is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_ROLES]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchRolesPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch repositoryRoles is not pending", () => {
|
||||
expect(isFetchRolesPending({})).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch repositoryRoles did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_ROLES]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchRolesFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch repositoryRoles 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);
|
||||
});
|
||||
});
|
||||
@@ -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/roles/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
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -151,6 +159,10 @@ 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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
Subtitle,
|
||||
Autocomplete,
|
||||
@@ -9,30 +15,28 @@ import {
|
||||
LabelWithHelpIcon,
|
||||
Radio
|
||||
} from "@scm-manager/ui-components";
|
||||
import RoleSelector from "../components/RoleSelector";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
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";
|
||||
import { findVerbsForRole } from "../modules/permissions";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
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,
|
||||
verbs: string[],
|
||||
role?: string,
|
||||
verbs?: string[],
|
||||
groupPermission: boolean,
|
||||
valid: boolean,
|
||||
value?: SelectValue,
|
||||
@@ -45,7 +49,8 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
|
||||
this.state = {
|
||||
name: "",
|
||||
verbs: props.availablePermissions.availableRoles[0].verbs,
|
||||
role: props.availableRoles[0].name,
|
||||
verbs: undefined,
|
||||
groupPermission: false,
|
||||
valid: true,
|
||||
value: undefined,
|
||||
@@ -90,6 +95,7 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderAutocompletionField = () => {
|
||||
const { t } = this.props;
|
||||
if (this.state.groupPermission) {
|
||||
@@ -133,19 +139,17 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, availablePermissions, loading } = this.props;
|
||||
const { t, availableRoles, availableVerbs, loading } = this.props;
|
||||
const { role, verbs, showAdvancedDialog } = this.state;
|
||||
|
||||
const { verbs, showAdvancedDialog } = this.state;
|
||||
const availableRoleNames = availableRoles.map(r => r.name);
|
||||
|
||||
const availableRoleNames = availablePermissions.availableRoles.map(
|
||||
r => r.name
|
||||
);
|
||||
const matchingRole = findMatchingRoleName(availablePermissions, verbs);
|
||||
const selectedVerbs = role ? findVerbsForRole(availableRoles, role) : verbs;
|
||||
|
||||
const advancedDialog = showAdvancedDialog ? (
|
||||
<AdvancedPermissionsDialog
|
||||
availableVerbs={availablePermissions.availableVerbs}
|
||||
selectedVerbs={verbs}
|
||||
availableVerbs={availableVerbs}
|
||||
selectedVerbs={selectedVerbs}
|
||||
onClose={this.closeAdvancedPermissionsDialog}
|
||||
onSubmit={this.submitAdvancedPermissionsDialog}
|
||||
/>
|
||||
@@ -187,7 +191,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">
|
||||
@@ -228,6 +232,7 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
|
||||
this.setState({
|
||||
showAdvancedDialog: false,
|
||||
role: undefined,
|
||||
verbs: newVerbs
|
||||
});
|
||||
};
|
||||
@@ -235,6 +240,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
|
||||
});
|
||||
@@ -245,7 +251,8 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
removeState = () => {
|
||||
this.setState({
|
||||
name: "",
|
||||
verbs: this.props.availablePermissions.availableRoles[0].verbs,
|
||||
role: this.props.availableRoles[0].name,
|
||||
verbs: undefined,
|
||||
valid: true,
|
||||
value: undefined
|
||||
});
|
||||
@@ -257,14 +264,13 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
verbs: selectedRole.verbs
|
||||
role: selectedRole.name,
|
||||
verbs: []
|
||||
});
|
||||
};
|
||||
|
||||
findAvailableRole = (roleName: string) => {
|
||||
return this.props.availablePermissions.availableRoles.find(
|
||||
role => role.name === roleName
|
||||
);
|
||||
return this.props.availableRoles.find(role => role.name === roleName);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
getDeletePermissionsFailure,
|
||||
getModifyPermissionsFailure,
|
||||
modifyPermissionReset,
|
||||
deletePermissionReset
|
||||
deletePermissionReset,
|
||||
getAvailableRepositoryRoles,
|
||||
getAvailableRepositoryVerbs
|
||||
} from "../modules/permissions";
|
||||
import {
|
||||
Loading,
|
||||
@@ -28,10 +30,10 @@ 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";
|
||||
@@ -39,11 +41,15 @@ import type { History } from "history";
|
||||
import { getPermissionsLink } from "../../modules/repos";
|
||||
import {
|
||||
getGroupAutoCompleteLink,
|
||||
getRepositoryRolesLink,
|
||||
getRepositoryVerbsLink,
|
||||
getUserAutoCompleteLink
|
||||
} from "../../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
availablePermissions: boolean,
|
||||
availableRepositoryRoles: RepositoryRole[],
|
||||
availableVerbs: string[],
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
loading: boolean,
|
||||
@@ -51,12 +57,17 @@ type Props = {
|
||||
permissions: PermissionCollection,
|
||||
hasPermissionToCreate: boolean,
|
||||
loadingCreatePermission: boolean,
|
||||
repositoryRolesLink: string,
|
||||
repositoryVerbsLink: string,
|
||||
permissionsLink: string,
|
||||
groupAutoCompleteLink: string,
|
||||
userAutoCompleteLink: string,
|
||||
|
||||
//dispatch functions
|
||||
fetchAvailablePermissionsIfNeeded: () => void,
|
||||
fetchAvailablePermissionsIfNeeded: (
|
||||
repositoryRolesLink: string,
|
||||
repositoryVerbsLink: string
|
||||
) => void,
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
|
||||
createPermission: (
|
||||
link: string,
|
||||
@@ -74,7 +85,6 @@ type Props = {
|
||||
history: History
|
||||
};
|
||||
|
||||
|
||||
class Permissions extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const {
|
||||
@@ -85,13 +95,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 +119,8 @@ class Permissions extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
availablePermissions,
|
||||
availableRepositoryRoles,
|
||||
availableVerbs,
|
||||
loading,
|
||||
error,
|
||||
permissions,
|
||||
@@ -134,7 +148,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 +189,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 +225,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 +258,16 @@ 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,
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
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
|
||||
findVerbsForRole
|
||||
} from "../modules/permissions";
|
||||
import { connect } from "react-redux";
|
||||
import type { History } from "history";
|
||||
@@ -22,7 +19,8 @@ import classNames from "classnames";
|
||||
import injectSheet from "react-jss";
|
||||
|
||||
type Props = {
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
availableRepositoryRoles: RepositoryRole[],
|
||||
availableRepositoryVerbs: string[],
|
||||
submitForm: Permission => void,
|
||||
modifyPermission: (
|
||||
permission: Permission,
|
||||
@@ -46,7 +44,6 @@ type Props = {
|
||||
};
|
||||
|
||||
type State = {
|
||||
role: string,
|
||||
permission: Permission,
|
||||
showAdvancedDialog: boolean
|
||||
};
|
||||
@@ -68,39 +65,34 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const defaultPermission = props.availablePermissions.availableRoles
|
||||
? props.availablePermissions.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 { availablePermissions, permission } = this.props;
|
||||
|
||||
const matchingRole = findMatchingRoleName(
|
||||
availablePermissions,
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -114,37 +106,41 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { role, permission, showAdvancedDialog } = this.state;
|
||||
const { permission, showAdvancedDialog } = this.state;
|
||||
const {
|
||||
t,
|
||||
availablePermissions,
|
||||
availableRepositoryRoles,
|
||||
availableRepositoryVerbs,
|
||||
loading,
|
||||
namespace,
|
||||
repoName,
|
||||
classes
|
||||
} = this.props;
|
||||
const availableRoleNames = availablePermissions.availableRoles.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>
|
||||
);
|
||||
|
||||
const advancedDialg = showAdvancedDialog ? (
|
||||
const selectedVerbs = permission.role
|
||||
? findVerbsForRole(availableRepositoryRoles, permission.role)
|
||||
: permission.verbs;
|
||||
|
||||
const advancedDialog = showAdvancedDialog ? (
|
||||
<AdvancedPermissionsDialog
|
||||
readOnly={readOnly}
|
||||
availableVerbs={availablePermissions.availableVerbs}
|
||||
selectedVerbs={permission.verbs}
|
||||
availableVerbs={availableRepositoryVerbs}
|
||||
selectedVerbs={selectedVerbs}
|
||||
onClose={this.closeAdvancedPermissionsDialog}
|
||||
onSubmit={this.submitAdvancedPermissionsDialog}
|
||||
/>
|
||||
@@ -152,9 +148,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 (
|
||||
@@ -177,7 +179,7 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
deletePermission={this.deletePermission}
|
||||
loading={this.props.deleteLoading}
|
||||
/>
|
||||
{advancedDialg}
|
||||
{advancedDialog}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -197,41 +199,41 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
|
||||
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
|
||||
const { permission } = this.state;
|
||||
const newRole = findMatchingRoleName(
|
||||
this.props.availablePermissions,
|
||||
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
|
||||
permission: { ...permission, role: role, verbs: undefined }
|
||||
},
|
||||
role: role
|
||||
},
|
||||
() => this.modifyPermission(selectedRole.verbs)
|
||||
() => this.modifyPermissionRole(role)
|
||||
);
|
||||
};
|
||||
|
||||
findAvailableRole = (roleName: string) => {
|
||||
return this.props.availablePermissions.availableRoles.find(
|
||||
role => role.name === roleName
|
||||
const { availableRepositoryRoles } = this.props;
|
||||
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(
|
||||
|
||||
@@ -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,45 @@ 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 +143,7 @@ export function fetchAvailablePending(): Action {
|
||||
}
|
||||
|
||||
export function fetchAvailableSuccess(
|
||||
available: AvailableRepositoryPermissions
|
||||
available: [RepositoryRole[], string[]]
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_AVAILABLE_SUCCESS,
|
||||
@@ -543,14 +565,28 @@ 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,
|
||||
repoName: string
|
||||
) {
|
||||
if (state.permissions && state.permissions[namespace + "/" + repoName]) {
|
||||
const permissions = state.permissions[namespace + "/" + repoName].entries;
|
||||
return permissions;
|
||||
return state.permissions[namespace + "/" + repoName].entries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,32 +740,16 @@ export function getModifyPermissionsFailure(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findMatchingRoleName(
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
verbs: string[]
|
||||
export function findVerbsForRole(
|
||||
availableRepositoryRoles: RepositoryRole[],
|
||||
roleName: string
|
||||
) {
|
||||
if (!verbs) {
|
||||
return "";
|
||||
}
|
||||
const matchingRole = availablePermissions.availableRoles.find(role => {
|
||||
return equalVerbs(role.verbs, verbs);
|
||||
});
|
||||
|
||||
const matchingRole = availableRepositoryRoles.find(
|
||||
role => roleName === role.name
|
||||
);
|
||||
if (matchingRole) {
|
||||
return matchingRole.name;
|
||||
return matchingRole.verbs;
|
||||
} else {
|
||||
return "";
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -75,10 +76,13 @@ 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.RepositoryRoleManager;
|
||||
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 +271,8 @@ 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);
|
||||
bind(RepositoryRoleManager.class).to(DefaultRepositoryRoleManager.class);
|
||||
|
||||
bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class,
|
||||
RepositoryManagerProvider.class);
|
||||
|
||||
@@ -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.security.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Target({TYPE})
|
||||
@Retention(RUNTIME)
|
||||
@Constraint(validatedBy = EitherRoleOrVerbsValidator.class)
|
||||
@Documented
|
||||
public @interface EitherRoleOrVerbs {
|
||||
|
||||
String message() default "permission must either have a role or a not empty set of verbs";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class EitherRoleOrVerbsValidator implements ConstraintValidator<EitherRoleOrVerbs, RepositoryPermissionDto> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EitherRoleOrVerbsValidator.class);
|
||||
|
||||
@Override
|
||||
public void initialize(EitherRoleOrVerbs constraintAnnotation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(RepositoryPermissionDto object, ConstraintValidatorContext constraintContext) {
|
||||
if (Strings.isNullOrEmpty(object.getRole())) {
|
||||
boolean result = object.getVerbs() != null && !object.getVerbs().isEmpty();
|
||||
LOG.trace("Validation result for permission with empty or no role: {}", result);
|
||||
return result;
|
||||
} else {
|
||||
boolean result = object.getVerbs() == null || object.getVerbs().isEmpty();
|
||||
LOG.trace("Validation result for permission with non empty role: {}", result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -58,10 +59,13 @@ 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()));
|
||||
if (RepositoryRolePermissions.read().isPermitted()) {
|
||||
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
|
||||
}
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> verbs;
|
||||
|
||||
private String role;
|
||||
|
||||
private boolean groupPermission = false;
|
||||
|
||||
public RepositoryPermissionDto(String permissionName, boolean groupPermission) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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 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<RepositoryRole, RepositoryRoleDto> 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}).
|
||||
*
|
||||
* <strong>Note:</strong> 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.
|
||||
*
|
||||
* <strong>Note:</strong> 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()));
|
||||
}
|
||||
}
|
||||
@@ -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<RepositoryRole, RepositoryRoleDto, RepositoryRoleToRepositoryRoleDtoMapper> {
|
||||
|
||||
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<RepositoryRole> pageResult) {
|
||||
return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink());
|
||||
}
|
||||
|
||||
Optional<String> createCreateLink() {
|
||||
return RepositoryRolePermissions.modify().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty();
|
||||
}
|
||||
|
||||
String createSelfLink() {
|
||||
return resourceLinks.repositoryRoleCollection().self();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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 org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class RepositoryRoleDto extends HalRepresentation {
|
||||
@NotEmpty
|
||||
private String name;
|
||||
@NoBlankStrings @NotEmpty
|
||||
private Collection<String> verbs;
|
||||
private String type;
|
||||
private Instant creationDate;
|
||||
private Instant lastModified;
|
||||
|
||||
RepositoryRoleDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<RepositoryRole, RepositoryRoleDto> 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.
|
||||
*
|
||||
* <strong>Note:</strong> 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.
|
||||
*
|
||||
* <strong>Note:</strong> 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.
|
||||
*
|
||||
* <strong>Note:</strong> 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));
|
||||
}
|
||||
}
|
||||
@@ -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/repositoryRoles/";
|
||||
|
||||
private final Provider<RepositoryRoleCollectionResource> repositoryRoleCollectionResource;
|
||||
private final Provider<RepositoryRoleResource> repositoryRoleResource;
|
||||
|
||||
@Inject
|
||||
public RepositoryRoleRootResource(Provider<RepositoryRoleCollectionResource> repositoryRoleCollectionResource,
|
||||
Provider<RepositoryRoleResource> repositoryRoleResource) {
|
||||
this.repositoryRoleCollectionResource = repositoryRoleCollectionResource;
|
||||
this.repositoryRoleResource = repositoryRoleResource;
|
||||
}
|
||||
|
||||
@Path("")
|
||||
public RepositoryRoleCollectionResource getRepositoryRoleCollectionResource() {
|
||||
return repositoryRoleCollectionResource.get();
|
||||
}
|
||||
|
||||
@Path("{name}")
|
||||
public RepositoryRoleResource getRepositoryRoleResource() {
|
||||
return repositoryRoleResource.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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.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<RepositoryRole, RepositoryRoleDto> {
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@Override
|
||||
public abstract RepositoryRoleDto map(RepositoryRole modelObject);
|
||||
|
||||
@ObjectFactory
|
||||
RepositoryRoleDto createDto(RepositoryRole repositoryRole) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName()));
|
||||
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())));
|
||||
}
|
||||
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), repositoryRole);
|
||||
|
||||
return new RepositoryRoleDto(linksBuilder.build(), embeddedBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,6 @@ class ResourceLinks {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UserCollectionLinks userCollection() {
|
||||
return new UserCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
@@ -522,8 +521,66 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -669,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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.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;
|
||||
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;
|
||||
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;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@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, RepositoryPermissionProvider repositoryPermissionProvider)
|
||||
{
|
||||
this.repositoryRoleDAO = repositoryRoleDAO;
|
||||
this.managerDaoAdapter = new ManagerDaoAdapter<>(repositoryRoleDAO);
|
||||
this.repositoryPermissionProvider = repositoryPermissionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryRole create(RepositoryRole repositoryRole) {
|
||||
assertNoSystemRole(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) {
|
||||
assertNoSystemRole(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) {
|
||||
assertNoSystemRole(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().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);
|
||||
|
||||
if (repositoryRole != null) {
|
||||
return repositoryRole.clone();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<RepositoryRole> findSystemRole(String id) {
|
||||
return repositoryPermissionProvider
|
||||
.availableRoles()
|
||||
.stream()
|
||||
.filter(role -> !repositoryRoleDAO.getType().equals(role.getType()))
|
||||
.filter(role -> role.getName().equals(id)).findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RepositoryRole> getAll() {
|
||||
List<RepositoryRole> repositoryRoles = new ArrayList<>();
|
||||
|
||||
if (!RepositoryRolePermissions.read().isPermitted()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) {
|
||||
repositoryRoles.add(repositoryRole.clone());
|
||||
}
|
||||
|
||||
return repositoryRoles;
|
||||
}
|
||||
|
||||
@Override
|
||||
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 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RepositoryRole> getAll(int start, int limit)
|
||||
{
|
||||
return getAll(null, start, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getLastModified()
|
||||
{
|
||||
return repositoryRoleDAO.getLastModified();
|
||||
}
|
||||
|
||||
private final RepositoryRoleDAO repositoryRoleDAO;
|
||||
private final ManagerDaoAdapter<RepositoryRole> managerDaoAdapter;
|
||||
private final RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
}
|
||||
@@ -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 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, permission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +217,34 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
}
|
||||
}
|
||||
|
||||
private void addRepositoryPermission(Builder<String> builder, Repository repository, User user, RepositoryPermission permission) {
|
||||
Collection<String> 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<String> getVerbs(RepositoryPermission permission) {
|
||||
return permission.getRole() == null? permission.getVerbs(): getVerbsForRole(permission.getRole());
|
||||
}
|
||||
|
||||
private Collection<String> getVerbsForRole(String roleName) {
|
||||
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) {
|
||||
Builder<String> builder = ImmutableSet.builder();
|
||||
|
||||
@@ -353,4 +372,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|
||||
/** security system */
|
||||
private final SecuritySystem securitySystem;
|
||||
|
||||
private final RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
}
|
||||
|
||||
@@ -1,147 +1,42 @@
|
||||
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.ArrayList;
|
||||
import java.util.AbstractList;
|
||||
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 java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.unmodifiableCollection;
|
||||
import java.util.List ;
|
||||
|
||||
public class RepositoryPermissionProvider {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepositoryPermissionProvider.class);
|
||||
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
|
||||
private final Collection<String> availableVerbs;
|
||||
private final Collection<RepositoryRole> availableRoles;
|
||||
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
|
||||
private final RepositoryRoleDAO repositoryRoleDAO;
|
||||
|
||||
@Inject
|
||||
public RepositoryPermissionProvider(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())));
|
||||
public RepositoryPermissionProvider(SystemRepositoryPermissionProvider systemRepositoryPermissionProvider, RepositoryRoleDAO repositoryRoleDAO) {
|
||||
this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider;
|
||||
this.repositoryRoleDAO = repositoryRoleDAO;
|
||||
}
|
||||
|
||||
public Collection<String> availableVerbs() {
|
||||
return availableVerbs;
|
||||
return systemRepositoryPermissionProvider.availableVerbs();
|
||||
}
|
||||
|
||||
public Collection<RepositoryRole> availableRoles() {
|
||||
return availableRoles;
|
||||
List<RepositoryRole> availableSystemRoles = systemRepositoryPermissionProvider.availableRoles();
|
||||
List<RepositoryRole> customRoles = repositoryRoleDAO.getAll();
|
||||
|
||||
return new AbstractList<RepositoryRole>() {
|
||||
@Override
|
||||
public RepositoryRole get(int index) {
|
||||
return index < availableSystemRoles.size()? availableSystemRoles.get(index): customRoles.get(index - availableSystemRoles.size());
|
||||
}
|
||||
|
||||
private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) {
|
||||
Collection<String> availableVerbs = new ArrayList<>();
|
||||
Collection<RoleDescriptor> availableRoles = new ArrayList<>();
|
||||
|
||||
try {
|
||||
JAXBContext context =
|
||||
JAXBContext.newInstance(RepositoryPermissionsRoot.class);
|
||||
|
||||
// Querying permissions from uberClassLoader returns also the permissions from plugin
|
||||
Enumeration<URL> descriptorEnum =
|
||||
pluginLoader.getUberClassLoader().getResources(REPOSITORY_PERMISSION_DESCRIPTOR);
|
||||
|
||||
while (descriptorEnum.hasMoreElements()) {
|
||||
URL descriptorUrl = descriptorEnum.nextElement();
|
||||
|
||||
logger.debug("read repository permission descriptor from {}", descriptorUrl);
|
||||
|
||||
RepositoryPermissionsRoot repositoryPermissionsRoot = parsePermissionDescriptor(context, descriptorUrl);
|
||||
availableVerbs.addAll(repositoryPermissionsRoot.verbs.verbs);
|
||||
mergeRolesInto(availableRoles, repositoryPermissionsRoot.roles.roles);
|
||||
@Override
|
||||
public int size() {
|
||||
return availableSystemRoles.size() + customRoles.size();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.error("could not read permission descriptors", ex);
|
||||
} catch (JAXBException ex) {
|
||||
logger.error(
|
||||
"could not create jaxb context to read permission descriptors", ex);
|
||||
}
|
||||
|
||||
return new AvailableRepositoryPermissions(availableVerbs, availableRoles);
|
||||
}
|
||||
|
||||
private static void mergeRolesInto(Collection<RoleDescriptor> targetRoles, List<RoleDescriptor> additionalRoles) {
|
||||
additionalRoles.forEach(r -> addOrMergeInto(targetRoles, r));
|
||||
}
|
||||
|
||||
private static void addOrMergeInto(Collection<RoleDescriptor> targetRoles, RoleDescriptor additionalRole) {
|
||||
Optional<RoleDescriptor> existingRole = targetRoles
|
||||
.stream()
|
||||
.filter(r -> r.name.equals(additionalRole.name))
|
||||
.findFirst();
|
||||
if (existingRole.isPresent()) {
|
||||
existingRole.get().verbs.verbs.addAll(additionalRole.verbs.verbs);
|
||||
} else {
|
||||
targetRoles.add(additionalRole);
|
||||
}
|
||||
}
|
||||
|
||||
private static RepositoryPermissionsRoot parsePermissionDescriptor(JAXBContext context, URL descriptorUrl) {
|
||||
try {
|
||||
RepositoryPermissionsRoot descriptorWrapper =
|
||||
(RepositoryPermissionsRoot) context.createUnmarshaller().unmarshal(
|
||||
descriptorUrl);
|
||||
logger.trace("repository permissions from {}: {}", descriptorUrl, descriptorWrapper.verbs.verbs);
|
||||
logger.trace("repository roles from {}: {}", descriptorUrl, descriptorWrapper.roles.roles);
|
||||
return descriptorWrapper;
|
||||
} catch (JAXBException ex) {
|
||||
logger.error("could not parse permission descriptor", ex);
|
||||
return new RepositoryPermissionsRoot();
|
||||
}
|
||||
}
|
||||
|
||||
private static class AvailableRepositoryPermissions {
|
||||
private final Collection<String> availableVerbs;
|
||||
private final Collection<RoleDescriptor> availableRoles;
|
||||
|
||||
private AvailableRepositoryPermissions(Collection<String> availableVerbs, Collection<RoleDescriptor> availableRoles) {
|
||||
this.availableVerbs = unmodifiableCollection(availableVerbs);
|
||||
this.availableRoles = unmodifiableCollection(availableRoles);
|
||||
}
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "repository-permissions")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class RepositoryPermissionsRoot {
|
||||
private VerbListDescriptor verbs = new VerbListDescriptor();
|
||||
private RoleListDescriptor roles = new RoleListDescriptor();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "verbs")
|
||||
private static class VerbListDescriptor {
|
||||
@XmlElement(name = "verb")
|
||||
private Set<String> verbs = new LinkedHashSet<>();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "roles")
|
||||
private static class RoleListDescriptor {
|
||||
@XmlElement(name = "role")
|
||||
private List<RoleDescriptor> roles = new ArrayList<>();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "role")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class RoleDescriptor {
|
||||
@XmlElement(name = "name")
|
||||
private String name;
|
||||
@XmlElement(name = "verbs")
|
||||
private VerbListDescriptor verbs = new VerbListDescriptor();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> verbs;
|
||||
|
||||
public RepositoryRole(String name, Collection<String> verbs) {
|
||||
this.name = name;
|
||||
this.verbs = verbs;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Collection<String> 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
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 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.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 static java.util.stream.Collectors.toList;
|
||||
|
||||
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 List<String> availableVerbs;
|
||||
private final List<RepositoryRole> availableRoles;
|
||||
|
||||
@Inject
|
||||
public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) {
|
||||
AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader);
|
||||
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<String> availableVerbs() {
|
||||
return availableVerbs;
|
||||
}
|
||||
|
||||
public List<RepositoryRole> availableRoles() {
|
||||
return availableRoles;
|
||||
}
|
||||
|
||||
private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) {
|
||||
Collection<String> availableVerbs = new ArrayList<>();
|
||||
Collection<RoleDescriptor> availableRoles = new ArrayList<>();
|
||||
|
||||
try {
|
||||
JAXBContext context =
|
||||
JAXBContext.newInstance(RepositoryPermissionsRoot.class);
|
||||
|
||||
// Querying permissions from uberClassLoader returns also the permissions from plugin
|
||||
Enumeration<URL> descriptorEnum =
|
||||
pluginLoader.getUberClassLoader().getResources(REPOSITORY_PERMISSION_DESCRIPTOR);
|
||||
|
||||
while (descriptorEnum.hasMoreElements()) {
|
||||
URL descriptorUrl = descriptorEnum.nextElement();
|
||||
|
||||
logger.debug("read repository permission descriptor from {}", descriptorUrl);
|
||||
|
||||
RepositoryPermissionsRoot repositoryPermissionsRoot = parsePermissionDescriptor(context, descriptorUrl);
|
||||
availableVerbs.addAll(repositoryPermissionsRoot.verbs.verbs);
|
||||
mergeRolesInto(availableRoles, repositoryPermissionsRoot.roles.roles);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.error("could not read permission descriptors", ex);
|
||||
} catch (JAXBException ex) {
|
||||
logger.error(
|
||||
"could not create jaxb context to read permission descriptors", ex);
|
||||
}
|
||||
|
||||
return new AvailableRepositoryPermissions(availableVerbs, availableRoles);
|
||||
}
|
||||
|
||||
private static void mergeRolesInto(Collection<RoleDescriptor> targetRoles, List<RoleDescriptor> additionalRoles) {
|
||||
additionalRoles.forEach(r -> addOrMergeInto(targetRoles, r));
|
||||
}
|
||||
|
||||
private static void addOrMergeInto(Collection<RoleDescriptor> targetRoles, RoleDescriptor additionalRole) {
|
||||
Optional<RoleDescriptor> existingRole = targetRoles
|
||||
.stream()
|
||||
.filter(r -> r.name.equals(additionalRole.name))
|
||||
.findFirst();
|
||||
if (existingRole.isPresent()) {
|
||||
existingRole.get().verbs.verbs.addAll(additionalRole.verbs.verbs);
|
||||
} else {
|
||||
targetRoles.add(additionalRole);
|
||||
}
|
||||
}
|
||||
|
||||
private static RepositoryPermissionsRoot parsePermissionDescriptor(JAXBContext context, URL descriptorUrl) {
|
||||
try {
|
||||
RepositoryPermissionsRoot descriptorWrapper =
|
||||
(RepositoryPermissionsRoot) context.createUnmarshaller().unmarshal(
|
||||
descriptorUrl);
|
||||
logger.trace("repository permissions from {}: {}", descriptorUrl, descriptorWrapper.verbs.verbs);
|
||||
logger.trace("repository roles from {}: {}", descriptorUrl, descriptorWrapper.roles.roles);
|
||||
return descriptorWrapper;
|
||||
} catch (JAXBException ex) {
|
||||
logger.error("could not parse permission descriptor", ex);
|
||||
return new RepositoryPermissionsRoot();
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> List<T> removeDuplicates(Collection<T> items) {
|
||||
return items.stream().distinct().collect(toList());
|
||||
}
|
||||
|
||||
private static class AvailableRepositoryPermissions {
|
||||
private final Collection<String> availableVerbs;
|
||||
private final Collection<RoleDescriptor> availableRoles;
|
||||
|
||||
private AvailableRepositoryPermissions(Collection<String> availableVerbs, Collection<RoleDescriptor> availableRoles) {
|
||||
this.availableVerbs = unmodifiableCollection(availableVerbs);
|
||||
this.availableRoles = unmodifiableCollection(availableRoles);
|
||||
}
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "repository-permissions")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class RepositoryPermissionsRoot {
|
||||
private VerbListDescriptor verbs = new VerbListDescriptor();
|
||||
private RoleListDescriptor roles = new RoleListDescriptor();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "verbs")
|
||||
private static class VerbListDescriptor {
|
||||
@XmlElement(name = "verb")
|
||||
private Set<String> verbs = new LinkedHashSet<>();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "roles")
|
||||
private static class RoleListDescriptor {
|
||||
@XmlElement(name = "role")
|
||||
private List<RoleDescriptor> roles = new ArrayList<>();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "role")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class RoleDescriptor {
|
||||
@XmlElement(name = "name")
|
||||
private String name;
|
||||
@XmlElement(name = "verbs")
|
||||
private VerbListDescriptor verbs = new VerbListDescriptor();
|
||||
}
|
||||
}
|
||||
@@ -66,5 +66,8 @@
|
||||
<permission>
|
||||
<value>configuration:read,write:*</value>
|
||||
</permission>
|
||||
<permission>
|
||||
<value>repositoryRole:read,write</value>
|
||||
</permission>
|
||||
|
||||
</permissions>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<RepositoryPermission> expectedPermissions, String userPermission) throws URISyntaxException {
|
||||
assertExpectedRequest(requestGETAllPermissions
|
||||
.expectedResponseStatus(200)
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
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.Arrays.asList;
|
||||
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 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
|
||||
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<RepositoryRole> modifyCaptor;
|
||||
@Captor
|
||||
private ArgumentCaptor<RepositoryRole> createCaptor;
|
||||
@Captor
|
||||
private ArgumentCaptor<RepositoryRole> 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(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
|
||||
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 shouldGetCustomRole() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains(
|
||||
"\"name\":\"" + CUSTOM_ROLE + "\"",
|
||||
"\"verbs\":[\"verb\"]",
|
||||
"\"self\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}",
|
||||
"\"update\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}",
|
||||
"\"delete\":{\"href\":\"/v2/repositoryRoles/" + 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/repositoryRoles/" + SYSTEM_ROLE + "\"}"
|
||||
)
|
||||
.doesNotContain(
|
||||
"\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}",
|
||||
"\"update\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "dent")
|
||||
public void shouldNotGetDeleteLinkWithoutPermission() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE);
|
||||
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 + CUSTOM_ROLE)
|
||||
.contentType(VndMediaType.REPOSITORY_ROLE)
|
||||
.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(CUSTOM_ROLE);
|
||||
assertThat(modifyCaptor.getValue().getVerbs()).containsExactly("write", "push");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotChangeRoleName() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE)
|
||||
.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/repositoryRoles/newRole"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeleteRole() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.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(CUSTOM_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\":\"" + CUSTOM_ROLE + "\"",
|
||||
"\"name\":\"" + SYSTEM_ROLE + "\"",
|
||||
"\"verbs\":[\"verb\"]",
|
||||
"\"verbs\":[\"admin\"]",
|
||||
"\"self\":{\"href\":\"/v2/repositoryRoles",
|
||||
"\"delete\":{\"href\":\"/v2/repositoryRoles/" + CUSTOM_ROLE + "\"}",
|
||||
"\"create\":{\"href\":\"/v2/repositoryRoles/\"}"
|
||||
)
|
||||
.doesNotContain(
|
||||
"\"delete\":{\"href\":\"/v2/repositoryRoles/" + SYSTEM_ROLE + "\"}"
|
||||
);
|
||||
}
|
||||
|
||||
@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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,9 @@ 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.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(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));
|
||||
|
||||
return resourceLinks;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
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.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 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;
|
||||
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.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);
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithAuthorizedUser {
|
||||
|
||||
@BeforeEach
|
||||
void authorizeUser() {
|
||||
when(subject.isPermitted("repositoryRole:read")).thenReturn(true);
|
||||
when(subject.isPermitted("repositoryRole:modify")).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_forNotExistingRole() {
|
||||
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"), "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)));
|
||||
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
|
||||
class WithUnauthorizedUser {
|
||||
|
||||
@BeforeEach
|
||||
void authorizeUser() {
|
||||
when(subject.isPermitted(any(String.class))).thenReturn(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,16 +49,19 @@ 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;
|
||||
|
||||
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;
|
||||
@@ -90,6 +93,9 @@ public class DefaultAuthorizationCollectorTest {
|
||||
@Mock
|
||||
private SecuritySystem securitySystem;
|
||||
|
||||
@Mock
|
||||
private RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
|
||||
private DefaultAuthorizationCollector collector;
|
||||
|
||||
@Rule
|
||||
@@ -101,11 +107,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 +124,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#collect()} from cache.
|
||||
* Tests {@link AuthorizationCollector#collect(PrincipalCollection)} from cache.
|
||||
*/
|
||||
@Test
|
||||
@SubjectAware(
|
||||
@@ -134,7 +140,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#collect()} with cache.
|
||||
* Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with cache.
|
||||
*/
|
||||
@Test
|
||||
@SubjectAware(
|
||||
@@ -148,7 +154,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#collect()} without permissions.
|
||||
* Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} without permissions.
|
||||
*/
|
||||
@Test
|
||||
@SubjectAware(
|
||||
@@ -165,7 +171,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#collect()} with repository permissions.
|
||||
* Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with repository permissions.
|
||||
*/
|
||||
@Test
|
||||
@SubjectAware(
|
||||
@@ -191,7 +197,76 @@ 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", singletonList("user"), "xml"),
|
||||
new RepositoryRole("group role", singletonList("group"), "xml"),
|
||||
new RepositoryRole("system role", singletonList("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 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.
|
||||
*/
|
||||
@Test
|
||||
@SubjectAware(
|
||||
|
||||
@@ -1,72 +1,51 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
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.util.ClassLoaders;
|
||||
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.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
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.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RepositoryPermissionProviderTest {
|
||||
|
||||
private RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
private String[] allVerbsFromRepositoryClass;
|
||||
@Mock
|
||||
SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
|
||||
@Mock
|
||||
RepositoryRoleDAO repositoryRoleDAO;
|
||||
|
||||
@InjectMocks
|
||||
RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
PluginLoader pluginLoader = mock(PluginLoader.class);
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
|
||||
repositoryPermissionProvider = new RepositoryPermissionProvider(pluginLoader);
|
||||
allVerbsFromRepositoryClass = Arrays.stream(RepositoryPermissions.class.getDeclaredFields())
|
||||
.filter(field -> field.getName().startsWith("ACTION_"))
|
||||
.filter(field -> !field.getName().equals("ACTION_HEALTHCHECK"))
|
||||
.map(this::getString)
|
||||
.filter(verb -> !"create".equals(verb))
|
||||
.toArray(String[]::new);
|
||||
@Test
|
||||
void shouldReturnVerbsFromSystem() {
|
||||
List<String> expectedVerbs = asList("verb1", "verb2");
|
||||
when(systemRepositoryPermissionProvider.availableVerbs()).thenReturn(expectedVerbs);
|
||||
|
||||
Collection<String> actualVerbs = repositoryPermissionProvider.availableVerbs();
|
||||
|
||||
assertThat(actualVerbs).isEqualTo(expectedVerbs);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadAvailableRoles() {
|
||||
assertThat(repositoryPermissionProvider.availableRoles()).isNotEmpty();
|
||||
assertThat(repositoryPermissionProvider.availableRoles()).allSatisfy(this::containsOnlyAvailableVerbs);
|
||||
}
|
||||
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));
|
||||
|
||||
private void containsOnlyAvailableVerbs(RepositoryRole role) {
|
||||
assertThat(role.getVerbs()).isSubsetOf(repositoryPermissionProvider.availableVerbs());
|
||||
}
|
||||
Collection<RepositoryRole> actualRoles = repositoryPermissionProvider.availableRoles();
|
||||
|
||||
@Test
|
||||
void shouldReadAvailableVerbsFromRepository() {
|
||||
assertThat(repositoryPermissionProvider.availableVerbs()).contains(allVerbsFromRepositoryClass);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMergeRepositoryRoles() {
|
||||
Collection<String> verbsInMergedRole = repositoryPermissionProvider
|
||||
.availableRoles()
|
||||
.stream()
|
||||
.filter(r -> "READ".equals(r.getName()))
|
||||
.findFirst()
|
||||
.get()
|
||||
.getVerbs();
|
||||
assertThat(verbsInMergedRole).contains("read", "pull", "test");
|
||||
}
|
||||
|
||||
private String getString(Field field) {
|
||||
try {
|
||||
return (String) field.get(null);
|
||||
} catch (IllegalAccessException e) {
|
||||
fail(e);
|
||||
return null;
|
||||
}
|
||||
assertThat(actualRoles).containsExactly(systemRole, daoRole);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class SystemRepositoryPermissionProviderTest {
|
||||
|
||||
private SystemRepositoryPermissionProvider repositoryPermissionProvider;
|
||||
private String[] allVerbsFromRepositoryClass;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
PluginLoader pluginLoader = mock(PluginLoader.class);
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
|
||||
repositoryPermissionProvider = new SystemRepositoryPermissionProvider(pluginLoader);
|
||||
allVerbsFromRepositoryClass = Arrays.stream(RepositoryPermissions.class.getDeclaredFields())
|
||||
.filter(field -> field.getName().startsWith("ACTION_"))
|
||||
.filter(field -> !field.getName().equals("ACTION_HEALTHCHECK"))
|
||||
.map(this::getString)
|
||||
.filter(verb -> !"create".equals(verb))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadAvailableRoles() {
|
||||
assertThat(repositoryPermissionProvider.availableRoles()).isNotEmpty();
|
||||
assertThat(repositoryPermissionProvider.availableRoles()).allSatisfy(this::containsOnlyAvailableVerbs);
|
||||
}
|
||||
|
||||
private void containsOnlyAvailableVerbs(RepositoryRole role) {
|
||||
assertThat(role.getVerbs()).isSubsetOf(repositoryPermissionProvider.availableVerbs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadAvailableVerbsFromRepository() {
|
||||
assertThat(repositoryPermissionProvider.availableVerbs()).contains(allVerbsFromRepositoryClass);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMergeRepositoryRoles() {
|
||||
Collection<String> verbsInMergedRole = repositoryPermissionProvider
|
||||
.availableRoles()
|
||||
.stream()
|
||||
.filter(r -> "READ".equals(r.getName()))
|
||||
.findFirst()
|
||||
.get()
|
||||
.getVerbs();
|
||||
assertThat(verbsInMergedRole).contains("read", "pull", "test");
|
||||
}
|
||||
|
||||
private String getString(Field field) {
|
||||
try {
|
||||
return (String) field.get(null);
|
||||
} catch (IllegalAccessException e) {
|
||||
fail(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user