Merged in feature/custom_roles_overview (pull request #250)

custom roles overview
This commit is contained in:
Rene Pfeuffer
2019-05-16 11:53:42 +00:00
90 changed files with 5248 additions and 547 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -50,6 +50,7 @@ import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet; import static java.util.Collections.unmodifiableSet;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -73,6 +74,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
private String name; private String name;
@XmlElement(name = "verb") @XmlElement(name = "verb")
private Set<String> verbs; private Set<String> verbs;
private String role;
/** /**
* This constructor exists for mapstruct and JAXB, only -- <b>do not use this in "normal" code</b>. * 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.name = name;
this.verbs = new LinkedHashSet<>(verbs); 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; this.groupPermission = groupPermission;
} }
@@ -116,8 +127,9 @@ public class RepositoryPermission implements PermissionObject, Serializable
final RepositoryPermission other = (RepositoryPermission) obj; final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name) return Objects.equal(name, other.name)
&& verbs.containsAll(other.verbs)
&& verbs.size() == other.verbs.size() && verbs.size() == other.verbs.size()
&& verbs.containsAll(other.verbs)
&& Objects.equal(role, other.role)
&& Objects.equal(groupPermission, other.groupPermission); && 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. // 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. // 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- //J-
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("name", name) .add("name", name)
.add("role", role)
.add("verbs", verbs) .add("verbs", verbs)
.add("groupPermission", groupPermission) .add("groupPermission", groupPermission)
.toString(); .toString();
@@ -173,6 +186,16 @@ public class RepositoryPermission implements PermissionObject, Serializable
return verbs == null ? emptyList() : Collections.unmodifiableSet(verbs); 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. * 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. * @throws IllegalStateException when modified after the value has been set once.
* *
* @deprecated Do not use this for "normal" code. * @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 @Deprecated
public void setGroupPermission(boolean groupPermission) 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. * @throws IllegalStateException when modified after the value has been set once.
* *
* @deprecated Do not use this for "normal" code. * @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 @Deprecated
public void setName(String name) public void setName(String name)
@@ -219,6 +244,22 @@ public class RepositoryPermission implements PermissionObject, Serializable
this.name = name; 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. * Use this for creation only. This will throw an {@link IllegalStateException} when modified.
* @throws IllegalStateException when modified after the value has been set once. * @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) { if (this.verbs != null) {
throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT);
} }
this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); this.verbs = verbs == null? emptySet(): unmodifiableSet(new LinkedHashSet<>(verbs));
} }
} }

View 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);
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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> {
}

View File

@@ -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;
}
}

View File

@@ -34,7 +34,7 @@ public class VndMediaType {
public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX;
public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX; public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX;
public static final String CONFIG = PREFIX + "config" + 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_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX;
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + 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 SOURCE = PREFIX + "source" + SUFFIX;
public static final String ERROR_TYPE = PREFIX + "error" + 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() { private VndMediaType() {
} }

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View 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"));
}
}

View File

@@ -201,7 +201,12 @@ public class ScmRequests {
return super.assertPropertyPathDoesNotExists(LINK_USERS); 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> { public class RepositoryResponse<PREV extends ModelResponse> extends ModelResponse<RepositoryResponse<PREV>, PREV> {

View File

@@ -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); 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) given(VndMediaType.REPOSITORY_PERMISSION)
.when() .when()
.content("{\n" + .content("{\n" +
"\t\"verbs\": " + permissionType.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" + "\t\"verbs\": " + verbs.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" +
"\t\"name\": \"" + name + "\",\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\"groupPermission\": false\n" +
"\t\n" + "\t\n" +
"}") "}")

View File

@@ -12,6 +12,8 @@ export type ButtonProps = {
fullWidth?: boolean, fullWidth?: boolean,
className?: string, className?: string,
children?: React.Node, children?: React.Node,
// context props
classes: any classes: any
}; };

View File

@@ -1,11 +0,0 @@
// @flow
export type RepositoryRole = {
name: string,
verbs: string[]
};
export type AvailableRepositoryPermissions = {
availableVerbs: string[],
availableRoles: RepositoryRole[]
};

View File

@@ -1,14 +1,15 @@
//@flow //@flow
import type {Links} from "./hal"; import type {Links} from "./hal";
export type PermissionCreateEntry = {
name: string,
role?: string,
verbs?: string[],
groupPermission: boolean
}
export type Permission = PermissionCreateEntry & { export type Permission = PermissionCreateEntry & {
_links: Links _links: Links
}; };
export type PermissionCreateEntry = {
name: string,
verbs: string[],
groupPermission: boolean
}
export type PermissionCollection = Permission[]; export type PermissionCollection = Permission[];

View 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
};

View File

@@ -25,6 +25,6 @@ export type { SubRepository, File } from "./Sources";
export type { SelectValue, AutocompleteObject } from "./Autocomplete"; export type { SelectValue, AutocompleteObject } from "./Autocomplete";
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions"; export type { RepositoryRole } from "./RepositoryRole";
export type { NamespaceStrategies } from "./NamespaceStrategies"; export type { NamespaceStrategies } from "./NamespaceStrategies";

View File

@@ -6,6 +6,33 @@
"errorTitle": "Fehler", "errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Einstellungen 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": { "config-form": {
"submit": "Speichern", "submit": "Speichern",
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!", "submit-success-notification": "Einstellungen wurden erfolgreich geändert!",

View File

@@ -119,6 +119,7 @@
"error-subtitle": "Unbekannter Fehler bei Berechtigung", "error-subtitle": "Unbekannter Fehler bei Berechtigung",
"name": "Benutzer oder Gruppe", "name": "Benutzer oder Gruppe",
"role": "Rolle", "role": "Rolle",
"custom": "CUSTOM",
"permissions": "Berechtigung", "permissions": "Berechtigung",
"group-permission": "Gruppenberechtigung", "group-permission": "Gruppenberechtigung",
"user-permission": "Benutzerberechtigung", "user-permission": "Benutzerberechtigung",

View File

@@ -6,6 +6,33 @@
"errorTitle": "Error", "errorTitle": "Error",
"errorSubtitle": "Unknown Config 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": { "config-form": {
"submit": "Submit", "submit": "Submit",
"submit-success-notification": "Configuration changed successfully!", "submit-success-notification": "Configuration changed successfully!",

View File

@@ -122,6 +122,7 @@
"error-subtitle": "Unknown permissions error", "error-subtitle": "Unknown permissions error",
"name": "User or group", "name": "User or group",
"role": "Role", "role": "Role",
"custom": "CUSTOM",
"permissions": "Permissions", "permissions": "Permissions",
"group-permission": "Group Permission", "group-permission": "Group Permission",
"user-permission": "User Permission", "user-permission": "User Permission",

View File

@@ -1,16 +1,18 @@
// @flow // @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; 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 { 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 type { History } from "history";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { compose } from "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 { 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 = { type Props = {
links: Links, links: Links,
@@ -33,6 +35,12 @@ class Config extends React.Component<Props> {
return this.stripEndingSlash(this.props.match.url); 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() { render() {
const { links, t } = this.props; const { links, t } = this.props;
@@ -46,12 +54,44 @@ class Config extends React.Component<Props> {
<Page> <Page>
<div className="columns"> <div className="columns">
<div className="column is-three-quarters"> <div className="column is-three-quarters">
<Switch>
<Route path={url} exact component={GlobalConfig} /> <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 <ExtensionPoint
name="config.route" name="config.route"
props={extensionProps} props={extensionProps}
renderAll={true} renderAll={true}
/> />
</Switch>
</div> </div>
<div className="column is-one-quarter"> <div className="column is-one-quarter">
<Navigation> <Navigation>
@@ -60,6 +100,11 @@ class Config extends React.Component<Props> {
to={`${url}`} to={`${url}`}
label={t("config.globalConfigurationNavLink")} label={t("config.globalConfigurationNavLink")}
/> />
<NavLink
to={`${url}/roles/`}
label={t("repositoryRole.navLink")}
activeWhenMatch={this.matchesRoles}
/>
<ExtensionPoint <ExtensionPoint
name="config.navigation" name="config.navigation"
props={extensionProps} props={extensionProps}

View 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);

View 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);

View File

@@ -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);

View 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;

View 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);

View 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));

View 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));

View 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));

View 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));

View 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))
);

View 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))
);

View 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);
}

View 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);
});
});

View File

@@ -15,6 +15,7 @@ import pending from "./modules/pending";
import failure from "./modules/failure"; import failure from "./modules/failure";
import permissions from "./repos/permissions/modules/permissions"; import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config"; import config from "./config/modules/config";
import roles from "./config/roles/modules/roles";
import namespaceStrategies from "./config/modules/namespaceStrategies"; import namespaceStrategies from "./config/modules/namespaceStrategies";
import indexResources from "./modules/indexResource"; import indexResources from "./modules/indexResource";
@@ -39,6 +40,7 @@ function createReduxStore(history: BrowserHistory) {
groups, groups,
auth, auth,
config, config,
roles,
sources, sources,
namespaceStrategies namespaceStrategies
}); });

View File

@@ -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_FAILURE = `${MODIFY_GROUP}_${types.FAILURE_SUFFIX}`;
export const MODIFY_GROUP_RESET = `${MODIFY_GROUP}_${types.RESET_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_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`;
export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`; export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`;
export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`; export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`;

View File

@@ -127,6 +127,14 @@ export function getUsersLink(state: Object) {
return getLink(state, "users"); 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) { export function getGroupsLink(state: Object) {
return getLink(state, "groups"); return getLink(state, "groups");
} }
@@ -151,6 +159,10 @@ export function getSvnConfigLink(state: Object) {
return getLink(state, "svnConfig"); return getLink(state, "svnConfig");
} }
export function getRolesLink(state: Object) {
return getLink(state, "repositoryRoles");
}
export function getUserAutoCompleteLink(state: Object): string { export function getUserAutoCompleteLink(state: Object): string {
const link = getLinkCollection(state, "autocomplete").find( const link = getLinkCollection(state, "autocomplete").find(
i => i.name === "users" i => i.name === "users"

View File

@@ -309,8 +309,7 @@ const reduceByBranchesSuccess = (state, payload) => {
if (response._embedded) { if (response._embedded) {
const branches = response._embedded.branches; const branches = response._embedded.branches;
const names = branches.map(b => b.name); response._embedded.branches = branches.map(b => b.name);
response._embedded.branches = names;
for (let branch of branches) { for (let branch of branches) {
byName[branch.name] = branch; byName[branch.name] = branch;
} }

View File

@@ -1,10 +1,19 @@
// @flow // @flow
import {FAILURE_SUFFIX, PENDING_SUFFIX, SUCCESS_SUFFIX} from "../../modules/types"; import {
FAILURE_SUFFIX,
PENDING_SUFFIX,
SUCCESS_SUFFIX
} from "../../modules/types";
import { apiClient, urls } from "@scm-manager/ui-components"; import { apiClient, urls } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending"; import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure"; import { getFailure } from "../../modules/failure";
import type {Action, Branch, PagedCollection, Repository} from "@scm-manager/ui-types"; import type {
Action,
Branch,
PagedCollection,
Repository
} from "@scm-manager/ui-types";
export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS"; export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS";
export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`; export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`;

View File

@@ -353,15 +353,13 @@ function normalizeByNamespaceAndName(
const reducerByNames = (state: Object, repository: Repository) => { const reducerByNames = (state: Object, repository: Repository) => {
const identifier = createIdentifier(repository); const identifier = createIdentifier(repository);
const newState = { return {
...state, ...state,
byNames: { byNames: {
...state.byNames, ...state.byNames,
[identifier]: repository [identifier]: repository
} }
}; };
return newState;
}; };
export default function reducer( export default function reducer(

View File

@@ -49,10 +49,7 @@ export function shouldFetchRepositoryTypes(state: Object) {
) { ) {
return false; return false;
} }
if (state.repositoryTypes && state.repositoryTypes.length > 0) { return !(state.repositoryTypes && state.repositoryTypes.length > 0);
return false;
}
return true;
} }
export function fetchRepositoryTypesPending(): Action { export function fetchRepositoryTypesPending(): Action {

View File

@@ -26,9 +26,11 @@ class AdvancedPermissionsDialog extends React.Component<Props, State> {
const verbs = {}; const verbs = {};
props.availableVerbs.forEach( props.availableVerbs.forEach(
verb => (verbs[verb] = props.selectedVerbs.includes(verb)) verb =>
(verbs[verb] = props.selectedVerbs
? props.selectedVerbs.includes(verb)
: false)
); );
this.state = { verbs }; this.state = { verbs };
} }

View File

@@ -1,6 +1,12 @@
// @flow // @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import type {
RepositoryRole,
PermissionCollection,
PermissionCreateEntry,
SelectValue
} from "@scm-manager/ui-types";
import { import {
Subtitle, Subtitle,
Autocomplete, Autocomplete,
@@ -9,30 +15,28 @@ import {
LabelWithHelpIcon, LabelWithHelpIcon,
Radio Radio
} from "@scm-manager/ui-components"; } 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 * as validator from "../components/permissionValidation";
import { findMatchingRoleName } from "../modules/permissions"; import RoleSelector from "../components/RoleSelector";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
import { findVerbsForRole } from "../modules/permissions";
type Props = { type Props = {
t: string => string, availableRoles: RepositoryRole[],
availablePermissions: AvailableRepositoryPermissions, availableVerbs: string[],
createPermission: (permission: PermissionCreateEntry) => void, createPermission: (permission: PermissionCreateEntry) => void,
loading: boolean, loading: boolean,
currentPermissions: PermissionCollection, currentPermissions: PermissionCollection,
groupAutoCompleteLink: string, groupAutoCompleteLink: string,
userAutoCompleteLink: string userAutoCompleteLink: string,
// Context props
t: string => string
}; };
type State = { type State = {
name: string, name: string,
verbs: string[], role?: string,
verbs?: string[],
groupPermission: boolean, groupPermission: boolean,
valid: boolean, valid: boolean,
value?: SelectValue, value?: SelectValue,
@@ -45,7 +49,8 @@ class CreatePermissionForm extends React.Component<Props, State> {
this.state = { this.state = {
name: "", name: "",
verbs: props.availablePermissions.availableRoles[0].verbs, role: props.availableRoles[0].name,
verbs: undefined,
groupPermission: false, groupPermission: false,
valid: true, valid: true,
value: undefined, value: undefined,
@@ -90,6 +95,7 @@ class CreatePermissionForm extends React.Component<Props, State> {
}); });
}); });
} }
renderAutocompletionField = () => { renderAutocompletionField = () => {
const { t } = this.props; const { t } = this.props;
if (this.state.groupPermission) { if (this.state.groupPermission) {
@@ -133,19 +139,17 @@ class CreatePermissionForm extends React.Component<Props, State> {
}; };
render() { 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( const selectedVerbs = role ? findVerbsForRole(availableRoles, role) : verbs;
r => r.name
);
const matchingRole = findMatchingRoleName(availablePermissions, verbs);
const advancedDialog = showAdvancedDialog ? ( const advancedDialog = showAdvancedDialog ? (
<AdvancedPermissionsDialog <AdvancedPermissionsDialog
availableVerbs={availablePermissions.availableVerbs} availableVerbs={availableVerbs}
selectedVerbs={verbs} selectedVerbs={selectedVerbs}
onClose={this.closeAdvancedPermissionsDialog} onClose={this.closeAdvancedPermissionsDialog}
onSubmit={this.submitAdvancedPermissionsDialog} onSubmit={this.submitAdvancedPermissionsDialog}
/> />
@@ -187,7 +191,7 @@ class CreatePermissionForm extends React.Component<Props, State> {
label={t("permission.role")} label={t("permission.role")}
helpText={t("permission.help.roleHelpText")} helpText={t("permission.help.roleHelpText")}
handleRoleChange={this.handleRoleChange} handleRoleChange={this.handleRoleChange}
role={matchingRole} role={role}
/> />
</div> </div>
<div className="column"> <div className="column">
@@ -228,6 +232,7 @@ class CreatePermissionForm extends React.Component<Props, State> {
submitAdvancedPermissionsDialog = (newVerbs: string[]) => { submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
this.setState({ this.setState({
showAdvancedDialog: false, showAdvancedDialog: false,
role: undefined,
verbs: newVerbs verbs: newVerbs
}); });
}; };
@@ -235,6 +240,7 @@ class CreatePermissionForm extends React.Component<Props, State> {
submit = e => { submit = e => {
this.props.createPermission({ this.props.createPermission({
name: this.state.name, name: this.state.name,
role: this.state.role,
verbs: this.state.verbs, verbs: this.state.verbs,
groupPermission: this.state.groupPermission groupPermission: this.state.groupPermission
}); });
@@ -245,7 +251,8 @@ class CreatePermissionForm extends React.Component<Props, State> {
removeState = () => { removeState = () => {
this.setState({ this.setState({
name: "", name: "",
verbs: this.props.availablePermissions.availableRoles[0].verbs, role: this.props.availableRoles[0].name,
verbs: undefined,
valid: true, valid: true,
value: undefined value: undefined
}); });
@@ -257,14 +264,13 @@ class CreatePermissionForm extends React.Component<Props, State> {
return; return;
} }
this.setState({ this.setState({
verbs: selectedRole.verbs role: selectedRole.name,
verbs: []
}); });
}; };
findAvailableRole = (roleName: string) => { findAvailableRole = (roleName: string) => {
return this.props.availablePermissions.availableRoles.find( return this.props.availableRoles.find(role => role.name === roleName);
role => role.name === roleName
);
}; };
} }

View File

@@ -19,7 +19,9 @@ import {
getDeletePermissionsFailure, getDeletePermissionsFailure,
getModifyPermissionsFailure, getModifyPermissionsFailure,
modifyPermissionReset, modifyPermissionReset,
deletePermissionReset deletePermissionReset,
getAvailableRepositoryRoles,
getAvailableRepositoryVerbs
} from "../modules/permissions"; } from "../modules/permissions";
import { import {
Loading, Loading,
@@ -28,10 +30,10 @@ import {
LabelWithHelpIcon LabelWithHelpIcon
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import type { import type {
AvailableRepositoryPermissions,
Permission, Permission,
PermissionCollection, PermissionCollection,
PermissionCreateEntry PermissionCreateEntry,
RepositoryRole
} from "@scm-manager/ui-types"; } from "@scm-manager/ui-types";
import SinglePermission from "./SinglePermission"; import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "./CreatePermissionForm"; import CreatePermissionForm from "./CreatePermissionForm";
@@ -39,11 +41,15 @@ import type { History } from "history";
import { getPermissionsLink } from "../../modules/repos"; import { getPermissionsLink } from "../../modules/repos";
import { import {
getGroupAutoCompleteLink, getGroupAutoCompleteLink,
getRepositoryRolesLink,
getRepositoryVerbsLink,
getUserAutoCompleteLink getUserAutoCompleteLink
} from "../../../modules/indexResource"; } from "../../../modules/indexResource";
type Props = { type Props = {
availablePermissions: AvailableRepositoryPermissions, availablePermissions: boolean,
availableRepositoryRoles: RepositoryRole[],
availableVerbs: string[],
namespace: string, namespace: string,
repoName: string, repoName: string,
loading: boolean, loading: boolean,
@@ -51,12 +57,17 @@ type Props = {
permissions: PermissionCollection, permissions: PermissionCollection,
hasPermissionToCreate: boolean, hasPermissionToCreate: boolean,
loadingCreatePermission: boolean, loadingCreatePermission: boolean,
repositoryRolesLink: string,
repositoryVerbsLink: string,
permissionsLink: string, permissionsLink: string,
groupAutoCompleteLink: string, groupAutoCompleteLink: string,
userAutoCompleteLink: string, userAutoCompleteLink: string,
//dispatch functions //dispatch functions
fetchAvailablePermissionsIfNeeded: () => void, fetchAvailablePermissionsIfNeeded: (
repositoryRolesLink: string,
repositoryVerbsLink: string
) => void,
fetchPermissions: (link: string, namespace: string, repoName: string) => void, fetchPermissions: (link: string, namespace: string, repoName: string) => void,
createPermission: ( createPermission: (
link: string, link: string,
@@ -74,7 +85,6 @@ type Props = {
history: History history: History
}; };
class Permissions extends React.Component<Props> { class Permissions extends React.Component<Props> {
componentDidMount() { componentDidMount() {
const { const {
@@ -85,13 +95,15 @@ class Permissions extends React.Component<Props> {
modifyPermissionReset, modifyPermissionReset,
createPermissionReset, createPermissionReset,
deletePermissionReset, deletePermissionReset,
permissionsLink permissionsLink,
repositoryRolesLink,
repositoryVerbsLink
} = this.props; } = this.props;
createPermissionReset(namespace, repoName); createPermissionReset(namespace, repoName);
modifyPermissionReset(namespace, repoName); modifyPermissionReset(namespace, repoName);
deletePermissionReset(namespace, repoName); deletePermissionReset(namespace, repoName);
fetchAvailablePermissionsIfNeeded(); fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink);
fetchPermissions(permissionsLink, namespace, repoName); fetchPermissions(permissionsLink, namespace, repoName);
} }
@@ -107,6 +119,8 @@ class Permissions extends React.Component<Props> {
render() { render() {
const { const {
availablePermissions, availablePermissions,
availableRepositoryRoles,
availableVerbs,
loading, loading,
error, error,
permissions, permissions,
@@ -134,7 +148,8 @@ class Permissions extends React.Component<Props> {
const createPermissionForm = hasPermissionToCreate ? ( const createPermissionForm = hasPermissionToCreate ? (
<CreatePermissionForm <CreatePermissionForm
availablePermissions={availablePermissions} availableRoles={availableRepositoryRoles}
availableVerbs={availableVerbs}
createPermission={permission => this.createPermission(permission)} createPermission={permission => this.createPermission(permission)}
loading={loadingCreatePermission} loading={loadingCreatePermission}
currentPermissions={permissions} currentPermissions={permissions}
@@ -174,7 +189,8 @@ class Permissions extends React.Component<Props> {
{permissions.map(permission => { {permissions.map(permission => {
return ( return (
<SinglePermission <SinglePermission
availablePermissions={availablePermissions} availableRepositoryRoles={availableRepositoryRoles}
availableRepositoryVerbs={availableVerbs}
key={permission.name + permission.groupPermission.toString()} key={permission.name + permission.groupPermission.toString()}
namespace={namespace} namespace={namespace}
repoName={repoName} repoName={repoName}
@@ -209,14 +225,23 @@ const mapStateToProps = (state, ownProps) => {
repoName repoName
); );
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
const repositoryRolesLink = getRepositoryRolesLink(state);
const repositoryVerbsLink = getRepositoryVerbsLink(state);
const permissionsLink = getPermissionsLink(state, namespace, repoName); const permissionsLink = getPermissionsLink(state, namespace, repoName);
const groupAutoCompleteLink = getGroupAutoCompleteLink(state); const groupAutoCompleteLink = getGroupAutoCompleteLink(state);
const userAutoCompleteLink = getUserAutoCompleteLink(state); const userAutoCompleteLink = getUserAutoCompleteLink(state);
const availablePermissions = getAvailablePermissions(state); const availablePermissions = getAvailablePermissions(state);
const availableRepositoryRoles = getAvailableRepositoryRoles(state);
const availableVerbs = getAvailableRepositoryVerbs(state);
return { return {
availablePermissions, availablePermissions,
availableRepositoryRoles,
availableVerbs,
namespace, namespace,
repoName, repoName,
repositoryRolesLink,
repositoryVerbsLink,
error, error,
loading, loading,
permissions, permissions,
@@ -233,8 +258,16 @@ const mapDispatchToProps = dispatch => {
fetchPermissions: (link: string, namespace: string, repoName: string) => { fetchPermissions: (link: string, namespace: string, repoName: string) => {
dispatch(fetchPermissions(link, namespace, repoName)); dispatch(fetchPermissions(link, namespace, repoName));
}, },
fetchAvailablePermissionsIfNeeded: () => { fetchAvailablePermissionsIfNeeded: (
dispatch(fetchAvailablePermissionsIfNeeded()); repositoryRolesLink: string,
repositoryVerbsLink: string
) => {
dispatch(
fetchAvailablePermissionsIfNeeded(
repositoryRolesLink,
repositoryVerbsLink
)
);
}, },
createPermission: ( createPermission: (
link: string, link: string,

View File

@@ -1,16 +1,13 @@
// @flow // @flow
import React from "react"; import React from "react";
import type { import type { RepositoryRole, Permission } from "@scm-manager/ui-types";
AvailableRepositoryPermissions,
Permission
} from "@scm-manager/ui-types";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { import {
modifyPermission, modifyPermission,
isModifyPermissionPending, isModifyPermissionPending,
deletePermission, deletePermission,
isDeletePermissionPending, isDeletePermissionPending,
findMatchingRoleName findVerbsForRole
} from "../modules/permissions"; } from "../modules/permissions";
import { connect } from "react-redux"; import { connect } from "react-redux";
import type { History } from "history"; import type { History } from "history";
@@ -22,7 +19,8 @@ import classNames from "classnames";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
type Props = { type Props = {
availablePermissions: AvailableRepositoryPermissions, availableRepositoryRoles: RepositoryRole[],
availableRepositoryVerbs: string[],
submitForm: Permission => void, submitForm: Permission => void,
modifyPermission: ( modifyPermission: (
permission: Permission, permission: Permission,
@@ -46,7 +44,6 @@ type Props = {
}; };
type State = { type State = {
role: string,
permission: Permission, permission: Permission,
showAdvancedDialog: boolean showAdvancedDialog: boolean
}; };
@@ -68,39 +65,34 @@ class SinglePermission extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const defaultPermission = props.availablePermissions.availableRoles const defaultPermission = props.availableRepositoryRoles
? props.availablePermissions.availableRoles[0] ? props.availableRepositoryRoles[0]
: {}; : {};
this.state = { this.state = {
permission: { permission: {
name: "", name: "",
role: undefined,
verbs: defaultPermission.verbs, verbs: defaultPermission.verbs,
groupPermission: false, groupPermission: false,
_links: {} _links: {}
}, },
role: defaultPermission.name,
showAdvancedDialog: false showAdvancedDialog: false
}; };
} }
componentDidMount() { componentDidMount() {
const { availablePermissions, permission } = this.props; const { permission } = this.props;
const matchingRole = findMatchingRoleName(
availablePermissions,
permission.verbs
);
if (permission) { if (permission) {
this.setState({ this.setState({
permission: { permission: {
name: permission.name, name: permission.name,
role: permission.role,
verbs: permission.verbs, verbs: permission.verbs,
groupPermission: permission.groupPermission, groupPermission: permission.groupPermission,
_links: permission._links _links: permission._links
}, }
role: matchingRole
}); });
} }
} }
@@ -114,37 +106,41 @@ class SinglePermission extends React.Component<Props, State> {
}; };
render() { render() {
const { role, permission, showAdvancedDialog } = this.state; const { permission, showAdvancedDialog } = this.state;
const { const {
t, t,
availablePermissions, availableRepositoryRoles,
availableRepositoryVerbs,
loading, loading,
namespace, namespace,
repoName, repoName,
classes classes
} = this.props; } = this.props;
const availableRoleNames = availablePermissions.availableRoles.map( const availableRoleNames =
r => r.name !!availableRepositoryRoles && availableRepositoryRoles.map(r => r.name);
);
const readOnly = !this.mayChangePermissions(); const readOnly = !this.mayChangePermissions();
const roleSelector = readOnly ? ( const roleSelector = readOnly ? (
<td>{role}</td> <td>{permission.role ? permission.role : t("permission.custom")}</td>
) : ( ) : (
<td> <td>
<RoleSelector <RoleSelector
handleRoleChange={this.handleRoleChange} handleRoleChange={this.handleRoleChange}
availableRoles={availableRoleNames} availableRoles={availableRoleNames}
role={role} role={permission.role}
loading={loading} loading={loading}
/> />
</td> </td>
); );
const advancedDialg = showAdvancedDialog ? ( const selectedVerbs = permission.role
? findVerbsForRole(availableRepositoryRoles, permission.role)
: permission.verbs;
const advancedDialog = showAdvancedDialog ? (
<AdvancedPermissionsDialog <AdvancedPermissionsDialog
readOnly={readOnly} readOnly={readOnly}
availableVerbs={availablePermissions.availableVerbs} availableVerbs={availableRepositoryVerbs}
selectedVerbs={permission.verbs} selectedVerbs={selectedVerbs}
onClose={this.closeAdvancedPermissionsDialog} onClose={this.closeAdvancedPermissionsDialog}
onSubmit={this.submitAdvancedPermissionsDialog} onSubmit={this.submitAdvancedPermissionsDialog}
/> />
@@ -152,9 +148,15 @@ class SinglePermission extends React.Component<Props, State> {
const iconType = const iconType =
permission && permission.groupPermission ? ( 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 ( return (
@@ -177,7 +179,7 @@ class SinglePermission extends React.Component<Props, State> {
deletePermission={this.deletePermission} deletePermission={this.deletePermission}
loading={this.props.deleteLoading} loading={this.props.deleteLoading}
/> />
{advancedDialg} {advancedDialog}
</td> </td>
</tr> </tr>
); );
@@ -197,41 +199,41 @@ class SinglePermission extends React.Component<Props, State> {
submitAdvancedPermissionsDialog = (newVerbs: string[]) => { submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
const { permission } = this.state; const { permission } = this.state;
const newRole = findMatchingRoleName(
this.props.availablePermissions,
newVerbs
);
this.setState( this.setState(
{ {
showAdvancedDialog: false, showAdvancedDialog: false,
permission: { ...permission, verbs: newVerbs }, permission: { ...permission, role: undefined, verbs: newVerbs }
role: newRole
}, },
() => this.modifyPermission(newVerbs) () => this.modifyPermissionVerbs(newVerbs)
); );
}; };
handleRoleChange = (role: string) => { handleRoleChange = (role: string) => {
const selectedRole = this.findAvailableRole(role); const { permission } = this.state;
this.setState( this.setState(
{ {
permission: { permission: { ...permission, role: role, verbs: undefined }
...this.state.permission,
verbs: selectedRole.verbs
}, },
role: role () => this.modifyPermissionRole(role)
},
() => this.modifyPermission(selectedRole.verbs)
); );
}; };
findAvailableRole = (roleName: string) => { findAvailableRole = (roleName: string) => {
return this.props.availablePermissions.availableRoles.find( const { availableRepositoryRoles } = this.props;
role => role.name === roleName return availableRepositoryRoles.find(role => role.name === roleName);
};
modifyPermissionRole = (role: string) => {
let permission = this.state.permission;
permission.role = role;
this.props.modifyPermission(
permission,
this.props.namespace,
this.props.repoName
); );
}; };
modifyPermission = (verbs: string[]) => { modifyPermissionVerbs = (verbs: string[]) => {
let permission = this.state.permission; let permission = this.state.permission;
permission.verbs = verbs; permission.verbs = verbs;
this.props.modifyPermission( this.props.modifyPermission(

View File

@@ -4,7 +4,7 @@ import type { Action } from "@scm-manager/ui-components";
import { apiClient } from "@scm-manager/ui-components"; import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../../modules/types"; import * as types from "../../../modules/types";
import type { import type {
AvailableRepositoryPermissions, RepositoryRole,
Permission, Permission,
PermissionCollection, PermissionCollection,
PermissionCreateEntry PermissionCreateEntry
@@ -12,7 +12,6 @@ import type {
import { isPending } from "../../../modules/pending"; import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure"; import { getFailure } from "../../../modules/failure";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { getLinks } from "../../../modules/indexResource";
export const FETCH_AVAILABLE = "scm/permissions/FETCH_AVAILABLE"; export const FETCH_AVAILABLE = "scm/permissions/FETCH_AVAILABLE";
export const FETCH_AVAILABLE_PENDING = `${FETCH_AVAILABLE}_${ export const FETCH_AVAILABLE_PENDING = `${FETCH_AVAILABLE}_${
@@ -78,22 +77,45 @@ const CONTENT_TYPE = "application/vnd.scmm-repositoryPermission+json";
// fetch available permissions // fetch available permissions
export function fetchAvailablePermissionsIfNeeded() { export function fetchAvailablePermissionsIfNeeded(
repositoryRolesLink: string,
repositoryVerbsLink: string
) {
return function(dispatch: any, getState: () => Object) { return function(dispatch: any, getState: () => Object) {
if (shouldFetchAvailablePermissions(getState())) { if (shouldFetchAvailablePermissions(getState())) {
return fetchAvailablePermissions(dispatch, getState); return fetchAvailablePermissions(
dispatch,
getState,
repositoryRolesLink,
repositoryVerbsLink
);
} }
}; };
} }
export function fetchAvailablePermissions( export function fetchAvailablePermissions(
dispatch: any, dispatch: any,
getState: () => Object getState: () => Object,
repositoryRolesLink: string,
repositoryVerbsLink: string
) { ) {
dispatch(fetchAvailablePending()); dispatch(fetchAvailablePending());
return apiClient return apiClient
.get(getLinks(getState()).availableRepositoryPermissions.href) .get(repositoryRolesLink)
.then(response => response.json()) .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 => { .then(available => {
dispatch(fetchAvailableSuccess(available)); dispatch(fetchAvailableSuccess(available));
}) })
@@ -121,7 +143,7 @@ export function fetchAvailablePending(): Action {
} }
export function fetchAvailableSuccess( export function fetchAvailableSuccess(
available: AvailableRepositoryPermissions available: [RepositoryRole[], string[]]
): Action { ): Action {
return { return {
type: FETCH_AVAILABLE_SUCCESS, 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( export function getPermissionsOfRepo(
state: Object, state: Object,
namespace: string, namespace: string,
repoName: string repoName: string
) { ) {
if (state.permissions && state.permissions[namespace + "/" + repoName]) { if (state.permissions && state.permissions[namespace + "/" + repoName]) {
const permissions = state.permissions[namespace + "/" + repoName].entries; return state.permissions[namespace + "/" + repoName].entries;
return permissions;
} }
} }
@@ -704,32 +740,16 @@ export function getModifyPermissionsFailure(
return null; return null;
} }
export function findMatchingRoleName( export function findVerbsForRole(
availablePermissions: AvailableRepositoryPermissions, availableRepositoryRoles: RepositoryRole[],
verbs: string[] roleName: string
) { ) {
if (!verbs) { const matchingRole = availableRepositoryRoles.find(
return ""; role => roleName === role.name
} );
const matchingRole = availablePermissions.availableRoles.find(role => {
return equalVerbs(role.verbs, verbs);
});
if (matchingRole) { if (matchingRole) {
return matchingRole.name; return matchingRole.verbs;
} else { } 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));
}

View File

@@ -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_FAILURE = `${MODIFY_USER}_${types.FAILURE_SUFFIX}`;
export const MODIFY_USER_RESET = `${MODIFY_USER}_${types.RESET_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_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`; export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`;
export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_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 reducerByName = (state: any, username: string, newUserState: any) => {
const newUsersByNames = { return {
...state, ...state,
[username]: newUserState [username]: newUserState
}; };
return newUsersByNames;
}; };
function listReducer(state: any = {}, action: any = {}) { function listReducer(state: any = {}, action: any = {}) {
@@ -341,7 +339,7 @@ function listReducer(state: any = {}, action: any = {}) {
...state, ...state,
entries: userNames, entries: userNames,
entry: { entry: {
userCreatePermission: action.payload._links.create ? true : false, userCreatePermission: !!action.payload._links.create,
page: action.payload.page, page: action.payload.page,
pageTotal: action.payload.pageTotal, pageTotal: action.payload.pageTotal,
_links: action.payload._links _links: action.payload._links
@@ -379,11 +377,10 @@ function byNamesReducer(state: any = {}, action: any = {}) {
return reducerByName(state, action.payload.name, action.payload); return reducerByName(state, action.payload.name, action.payload);
case DELETE_USER_SUCCESS: case DELETE_USER_SUCCESS:
const newUserByNames = deleteUserInUsersByNames( return deleteUserInUsersByNames(
state, state,
action.payload.name action.payload.name
); );
return newUserByNames;
default: default:
return state; return state;
@@ -417,11 +414,7 @@ export const selectListAsCollection = (state: Object): PagedCollection => {
}; };
export const isPermittedToCreateUsers = (state: Object): boolean => { export const isPermittedToCreateUsers = (state: Object): boolean => {
const permission = selectListEntry(state).userCreatePermission; return !!selectListEntry(state).userCreatePermission;
if (permission) {
return true;
}
return false;
}; };
export function getUsersFromState(state: Object) { export function getUsersFromState(state: Object) {

View File

@@ -4,49 +4,49 @@ import thunk from "redux-thunk";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import reducer, { import reducer, {
CREATE_USER_FAILURE, FETCH_USERS,
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_PENDING, FETCH_USERS_PENDING,
FETCH_USERS_SUCCESS, 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, fetchUserByLink,
fetchUserByName, fetchUserByName,
fetchUserSuccess, fetchUserSuccess,
isFetchUserPending,
getFetchUserFailure, getFetchUserFailure,
fetchUsers, createUser,
fetchUsersSuccess,
isFetchUsersPending,
selectListAsCollection,
isPermittedToCreateUsers,
MODIFY_USER,
MODIFY_USER_FAILURE,
MODIFY_USER_PENDING,
MODIFY_USER_SUCCESS,
modifyUser,
getUsersFromState,
FETCH_USERS,
getFetchUsersFailure,
FETCH_USER,
CREATE_USER,
isCreateUserPending, isCreateUserPending,
getCreateUserFailure, getCreateUserFailure,
getUserByName, getUserByName,
modifyUser,
isModifyUserPending, isModifyUserPending,
getModifyUserFailure, getModifyUserFailure,
DELETE_USER, deleteUser,
isDeleteUserPending, isDeleteUserPending,
getDeleteUserFailure deleteUserSuccess,
getDeleteUserFailure,
selectListAsCollection,
isPermittedToCreateUsers
} from "./users"; } from "./users";
const userZaphod = { const userZaphod = {
@@ -302,7 +302,7 @@ describe("users fetch()", () => {
}); });
it("should fail updating user on HTTP 500", () => { it("should fail updating user on HTTP 500", () => {
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", { fetchMock.putOnce(USER_ZAPHOD_URL, {
status: 500 status: 500
}); });
@@ -316,7 +316,7 @@ describe("users fetch()", () => {
}); });
it("should delete successfully user zaphod", () => { it("should delete successfully user zaphod", () => {
fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", { fetchMock.deleteOnce(USER_ZAPHOD_URL, {
status: 204 status: 204
}); });
@@ -331,7 +331,7 @@ describe("users fetch()", () => {
}); });
it("should call the callback, after successful delete", () => { it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", { fetchMock.deleteOnce(USER_ZAPHOD_URL, {
status: 204 status: 204
}); });
@@ -347,7 +347,7 @@ describe("users fetch()", () => {
}); });
it("should fail to delete user zaphod", () => { it("should fail to delete user zaphod", () => {
fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", { fetchMock.deleteOnce(USER_ZAPHOD_URL, {
status: 500 status: 500
}); });

View File

@@ -7,6 +7,8 @@ import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
public class ManagerDaoAdapter<T extends ModelObject> { public class ManagerDaoAdapter<T extends ModelObject> {
private final GenericDAO<T> dao; private final GenericDAO<T> dao;
@@ -19,6 +21,9 @@ public class ManagerDaoAdapter<T extends ModelObject> {
T notModified = dao.get(object.getId()); T notModified = dao.get(object.getId());
if (notModified != null) { if (notModified != null) {
permissionCheck.apply(notModified).check(); permissionCheck.apply(notModified).check();
doThrow().violation("type must not be changed").when(!notModified.getType().equals(object.getType()));
AssertUtil.assertIsValid(object); AssertUtil.assertIsValid(object);
beforeUpdate.handle(notModified); beforeUpdate.handle(notModified);

View File

@@ -67,6 +67,7 @@ import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginManager;
import sonia.scm.repository.DefaultRepositoryManager; import sonia.scm.repository.DefaultRepositoryManager;
import sonia.scm.repository.DefaultRepositoryProvider; import sonia.scm.repository.DefaultRepositoryProvider;
import sonia.scm.repository.DefaultRepositoryRoleManager;
import sonia.scm.repository.HealthCheckContextListener; import sonia.scm.repository.HealthCheckContextListener;
import sonia.scm.repository.NamespaceStrategy; import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.NamespaceStrategyProvider; import sonia.scm.repository.NamespaceStrategyProvider;
@@ -75,10 +76,13 @@ import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryManagerProvider; import sonia.scm.repository.RepositoryManagerProvider;
import sonia.scm.repository.RepositoryProvider; 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.HookContextFactory;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.repository.xml.XmlRepositoryRoleDAO;
import sonia.scm.schedule.QuartzScheduler; import sonia.scm.schedule.QuartzScheduler;
import sonia.scm.schedule.Scheduler; import sonia.scm.schedule.Scheduler;
import sonia.scm.security.AccessTokenCookieIssuer; import sonia.scm.security.AccessTokenCookieIssuer;
@@ -267,6 +271,8 @@ public class ScmServletModule extends ServletModule
bind(GroupDAO.class, XmlGroupDAO.class); bind(GroupDAO.class, XmlGroupDAO.class);
bind(UserDAO.class, XmlUserDAO.class); bind(UserDAO.class, XmlUserDAO.class);
bind(RepositoryDAO.class, XmlRepositoryDAO.class); bind(RepositoryDAO.class, XmlRepositoryDAO.class);
bind(RepositoryRoleDAO.class, XmlRepositoryRoleDAO.class);
bind(RepositoryRoleManager.class).to(DefaultRepositoryRoleManager.class);
bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class, bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class,
RepositoryManagerProvider.class); RepositoryManagerProvider.class);

View File

@@ -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);
}
}

View File

@@ -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 {};
}

View File

@@ -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;
}
}
}

View File

@@ -8,6 +8,7 @@ import org.apache.shiro.SecurityUtils;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.group.GroupPermissions; import sonia.scm.group.GroupPermissions;
import sonia.scm.repository.RepositoryRolePermissions;
import sonia.scm.security.PermissionPermissions; import sonia.scm.security.PermissionPermissions;
import sonia.scm.user.UserPermissions; import sonia.scm.user.UserPermissions;
@@ -58,10 +59,13 @@ public class IndexDtoGenerator extends HalAppenderMapper {
if (PermissionPermissions.list().isPermitted()) { if (PermissionPermissions.list().isPermitted()) {
builder.single(link("permissions", resourceLinks.permissions().self())); 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("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self())); builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
if (RepositoryRolePermissions.read().isPermitted()) {
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
}
} else { } else {
builder.single(link("login", resourceLinks.authentication().jsonLogin())); builder.single(link("login", resourceLinks.authentication().jsonLogin()));
} }

View File

@@ -28,6 +28,10 @@ public class MapperModule extends AbstractModule {
bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass()); bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass());
bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.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(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(DefaultChangesetToChangesetDtoMapper.class).getClass());
bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass()); bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass());

View File

@@ -106,7 +106,7 @@ public class RepositoryCollectionResource {
private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) { private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) {
Repository repository = dtoToRepositoryMapper.map(repositoryDto, null); Repository repository = dtoToRepositoryMapper.map(repositoryDto, null);
repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), singletonList("*"), false))); repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), "OWNER", false)));
return repository; return repository;
} }

View File

@@ -14,6 +14,7 @@ import javax.validation.constraints.Pattern;
import java.util.Collection; import java.util.Collection;
@Getter @Setter @ToString @NoArgsConstructor @Getter @Setter @ToString @NoArgsConstructor
@EitherRoleOrVerbs
public class RepositoryPermissionDto extends HalRepresentation { public class RepositoryPermissionDto extends HalRepresentation {
public static final String GROUP_PREFIX = "@"; public static final String GROUP_PREFIX = "@";
@@ -21,9 +22,11 @@ public class RepositoryPermissionDto extends HalRepresentation {
@Pattern(regexp = ValidationUtil.REGEX_NAME) @Pattern(regexp = ValidationUtil.REGEX_NAME)
private String name; private String name;
@NotEmpty @NoBlankStrings
private Collection<String> verbs; private Collection<String> verbs;
private String role;
private boolean groupPermission = false; private boolean groupPermission = false;
public RepositoryPermissionDto(String permissionName, boolean groupPermission) { public RepositoryPermissionDto(String permissionName, boolean groupPermission) {

View File

@@ -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()));
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -12,18 +12,18 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces; 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) @Path(RepositoryVerbResource.PATH)
public class RepositoryPermissionResource { public class RepositoryVerbResource {
static final String PATH = "v2/repositoryPermissions/"; static final String PATH = "v2/repositoryVerbs/";
private final RepositoryPermissionProvider repositoryPermissionProvider; private final RepositoryPermissionProvider repositoryPermissionProvider;
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
@Inject @Inject
public RepositoryPermissionResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { public RepositoryVerbResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) {
this.repositoryPermissionProvider = repositoryPermissionProvider; this.repositoryPermissionProvider = repositoryPermissionProvider;
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
} }
@@ -34,10 +34,11 @@ public class RepositoryPermissionResource {
@ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
@Produces(VndMediaType.REPOSITORY_PERMISSION_COLLECTION) @Produces(VndMediaType.REPOSITORY_VERB_COLLECTION)
public AvailableRepositoryPermissionsDto get() { public RepositoryVerbsDto getAll() {
AvailableRepositoryPermissionsDto dto = new AvailableRepositoryPermissionsDto(repositoryPermissionProvider.availableVerbs(), repositoryPermissionProvider.availableRoles()); return new RepositoryVerbsDto(
dto.add(Links.linkingTo().self(resourceLinks.availableRepositoryPermissions().self()).build()); Links.linkingTo().self(resourceLinks.repositoryVerbs().self()).build(),
return dto; repositoryPermissionProvider.availableVerbs()
);
} }
} }

View File

@@ -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;
}
}

View File

@@ -172,7 +172,6 @@ class ResourceLinks {
} }
} }
UserCollectionLinks userCollection() { UserCollectionLinks userCollection() {
return new UserCollectionLinks(scmPathInfoStore.get()); return new UserCollectionLinks(scmPathInfoStore.get());
} }
@@ -522,8 +521,66 @@ class ResourceLinks {
public String content(String namespace, String name, String revision, String path) { 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); 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() { public RepositoryPermissionLinks repositoryPermission() {
@@ -669,20 +726,4 @@ class ResourceLinks {
return permissionsLinkBuilder.method("getAll").parameters().href(); 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();
}
}
} }

View File

@@ -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;
}

View File

@@ -52,7 +52,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache; import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupNames; import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupPermissions; import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
@@ -64,7 +63,6 @@ import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import java.util.Collection; import java.util.Collection;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -90,18 +88,19 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
/** /**
* Constructs ... * Constructs ...
*
* @param cacheManager * @param cacheManager
* @param repositoryDAO * @param repositoryDAO
* @param securitySystem * @param securitySystem
* @param repositoryPermissionProvider
*/ */
@Inject @Inject
public DefaultAuthorizationCollector(CacheManager cacheManager, public DefaultAuthorizationCollector(CacheManager cacheManager,
RepositoryDAO repositoryDAO, SecuritySystem securitySystem) RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider)
{ {
this.cache = cacheManager.getCache(CACHE_NAME); this.cache = cacheManager.getCache(CACHE_NAME);
this.repositoryDAO = repositoryDAO; this.repositoryDAO = repositoryDAO;
this.securitySystem = securitySystem; this.securitySystem = securitySystem;
this.repositoryPermissionProvider = repositoryPermissionProvider;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -201,16 +200,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
for (RepositoryPermission permission : repositoryPermissions) for (RepositoryPermission permission : repositoryPermissions)
{ {
hasPermission = isUserPermitted(user, groups, permission); hasPermission = isUserPermitted(user, groups, permission);
if (hasPermission && !permission.getVerbs().isEmpty()) if (hasPermission) {
{ addRepositoryPermission(builder, repository, user, permission);
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);
} }
} }
@@ -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) { private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) {
Builder<String> builder = ImmutableSet.builder(); Builder<String> builder = ImmutableSet.builder();
@@ -353,4 +372,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
/** security system */ /** security system */
private final SecuritySystem securitySystem; private final SecuritySystem securitySystem;
private final RepositoryPermissionProvider repositoryPermissionProvider;
} }

View File

@@ -1,147 +1,42 @@
package sonia.scm.security; package sonia.scm.security;
import com.google.inject.Inject; import com.google.inject.Inject;
import org.slf4j.Logger; import sonia.scm.repository.RepositoryRole;
import org.slf4j.LoggerFactory; import sonia.scm.repository.RepositoryRoleDAO;
import sonia.scm.plugin.PluginLoader;
import javax.xml.bind.JAXBContext; import java.util.AbstractList;
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.Collection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List ; import java.util.List ;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.Collections.unmodifiableCollection;
public class RepositoryPermissionProvider { public class RepositoryPermissionProvider {
private static final Logger logger = LoggerFactory.getLogger(RepositoryPermissionProvider.class); private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml"; private final RepositoryRoleDAO repositoryRoleDAO;
private final Collection<String> availableVerbs;
private final Collection<RepositoryRole> availableRoles;
@Inject @Inject
public RepositoryPermissionProvider(PluginLoader pluginLoader) { public RepositoryPermissionProvider(SystemRepositoryPermissionProvider systemRepositoryPermissionProvider, RepositoryRoleDAO repositoryRoleDAO) {
AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader); this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider;
this.availableVerbs = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableVerbs)); this.repositoryRoleDAO = repositoryRoleDAO;
this.availableRoles = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs)).collect(Collectors.toList())));
} }
public Collection<String> availableVerbs() { public Collection<String> availableVerbs() {
return availableVerbs; return systemRepositoryPermissionProvider.availableVerbs();
} }
public Collection<RepositoryRole> availableRoles() { 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) { @Override
Collection<String> availableVerbs = new ArrayList<>(); public int size() {
Collection<RoleDescriptor> availableRoles = new ArrayList<>(); return availableSystemRoles.size() + customRoles.size();
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 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();
} }
} }

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -66,5 +66,8 @@
<permission> <permission>
<value>configuration:read,write:*</value> <value>configuration:read,write:*</value>
</permission> </permission>
<permission>
<value>repositoryRole:read,write</value>
</permission>
</permissions> </permissions>

View File

@@ -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" "unknown": "Unbekannte Berechtigung"
}, },
"verbs": { "verbs": {

View File

@@ -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" "unknown": "Unknown permission"
}, },
"verbs": { "verbs": {

View File

@@ -2,8 +2,6 @@ package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; 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.common.collect.ImmutableList;
import com.google.inject.util.Providers; import com.google.inject.util.Providers;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
@@ -21,7 +19,6 @@ import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@@ -35,6 +32,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.ws.rs.HttpMethod;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; 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; import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
@Slf4j @Slf4j
@SubjectAware(
username = "trillian",
password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAMESPACE = "repo_namespace";
private static final String REPOSITORY_NAME = "repo"; private static final String REPOSITORY_NAME = "repo";
@@ -114,9 +107,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
private Dispatcher dispatcher; private Dispatcher dispatcher;
@Rule
public ShiroRule shiro = new ShiroRule();
@Mock @Mock
private RepositoryManager repositoryManager; private RepositoryManager repositoryManager;
@@ -363,6 +353,69 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); 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 { private void assertGettingExpectedPermissions(ImmutableList<RepositoryPermission> expectedPermissions, String userPermission) throws URISyntaxException {
assertExpectedRequest(requestGETAllPermissions assertExpectedRequest(requestGETAllPermissions
.expectedResponseStatus(200) .expectedResponseStatus(200)

View File

@@ -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();
}
}

View File

@@ -332,7 +332,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.hasSize(1) .hasSize(1)
.allSatisfy(p -> { .allSatisfy(p -> {
assertThat(p.getName()).isEqualTo("trillian"); assertThat(p.getName()).isEqualTo("trillian");
assertThat(p.getVerbs()).containsExactly("*"); assertThat(p.getRole()).isEqualTo("OWNER");
}); });
} }

View File

@@ -42,8 +42,9 @@ public class ResourceLinksMock {
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo)); when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(uriInfo));
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo));
when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo));
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo));
return resourceLinks; return resourceLinks;

View File

@@ -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();
}
}
}

View File

@@ -33,10 +33,10 @@ package sonia.scm.security;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
@@ -49,16 +49,19 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.cache.Cache; import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupNames; import sonia.scm.group.GroupNames;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryRole;
import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserTestData; import sonia.scm.user.UserTestData;
import java.util.Collections;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
@@ -90,6 +93,9 @@ public class DefaultAuthorizationCollectorTest {
@Mock @Mock
private SecuritySystem securitySystem; private SecuritySystem securitySystem;
@Mock
private RepositoryPermissionProvider repositoryPermissionProvider;
private DefaultAuthorizationCollector collector; private DefaultAuthorizationCollector collector;
@Rule @Rule
@@ -101,11 +107,11 @@ public class DefaultAuthorizationCollectorTest {
@Before @Before
public void setUp(){ public void setUp(){
when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache); 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 @Test
@SubjectAware @SubjectAware
@@ -118,7 +124,7 @@ public class DefaultAuthorizationCollectorTest {
} }
/** /**
* Tests {@link AuthorizationCollector#collect()} from cache. * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} from cache.
*/ */
@Test @Test
@SubjectAware( @SubjectAware(
@@ -134,7 +140,7 @@ public class DefaultAuthorizationCollectorTest {
} }
/** /**
* Tests {@link AuthorizationCollector#collect()} with cache. * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with cache.
*/ */
@Test @Test
@SubjectAware( @SubjectAware(
@@ -148,7 +154,7 @@ public class DefaultAuthorizationCollectorTest {
} }
/** /**
* Tests {@link AuthorizationCollector#collect()} without permissions. * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} without permissions.
*/ */
@Test @Test
@SubjectAware( @SubjectAware(
@@ -165,7 +171,7 @@ public class DefaultAuthorizationCollectorTest {
} }
/** /**
* Tests {@link AuthorizationCollector#collect()} with repository permissions. * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with repository permissions.
*/ */
@Test @Test
@SubjectAware( @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 @Test
@SubjectAware( @SubjectAware(

View File

@@ -1,72 +1,51 @@
package sonia.scm.security; package sonia.scm.security;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sonia.scm.plugin.PluginLoader; import org.junit.jupiter.api.extension.ExtendWith;
import sonia.scm.repository.RepositoryPermissions; import org.mockito.InjectMocks;
import sonia.scm.util.ClassLoaders; 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.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.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; import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RepositoryPermissionProviderTest { class RepositoryPermissionProviderTest {
private RepositoryPermissionProvider repositoryPermissionProvider; @Mock
private String[] allVerbsFromRepositoryClass; SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
@Mock
RepositoryRoleDAO repositoryRoleDAO;
@InjectMocks
RepositoryPermissionProvider repositoryPermissionProvider;
@BeforeEach @Test
void init() { void shouldReturnVerbsFromSystem() {
PluginLoader pluginLoader = mock(PluginLoader.class); List<String> expectedVerbs = asList("verb1", "verb2");
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); when(systemRepositoryPermissionProvider.availableVerbs()).thenReturn(expectedVerbs);
repositoryPermissionProvider = new RepositoryPermissionProvider(pluginLoader);
allVerbsFromRepositoryClass = Arrays.stream(RepositoryPermissions.class.getDeclaredFields()) Collection<String> actualVerbs = repositoryPermissionProvider.availableVerbs();
.filter(field -> field.getName().startsWith("ACTION_"))
.filter(field -> !field.getName().equals("ACTION_HEALTHCHECK")) assertThat(actualVerbs).isEqualTo(expectedVerbs);
.map(this::getString)
.filter(verb -> !"create".equals(verb))
.toArray(String[]::new);
} }
@Test @Test
void shouldReadAvailableRoles() { void shouldReturnJoinedRolesFromSystemAndDao() {
assertThat(repositoryPermissionProvider.availableRoles()).isNotEmpty(); RepositoryRole systemRole = new RepositoryRole("roleSystem", singletonList("verb1"), "system");
assertThat(repositoryPermissionProvider.availableRoles()).allSatisfy(this::containsOnlyAvailableVerbs); 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) { Collection<RepositoryRole> actualRoles = repositoryPermissionProvider.availableRoles();
assertThat(role.getVerbs()).isSubsetOf(repositoryPermissionProvider.availableVerbs());
}
@Test assertThat(actualRoles).containsExactly(systemRole, daoRole);
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;
}
} }
} }

View File

@@ -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;
}
}
}

View File

@@ -8,7 +8,7 @@ user = secret, user
[roles] [roles]
admin = * admin = *
creator = repository:create creator = repository:create,repositoryRole:read
heartOfGold = "repository:read,modify,delete:hof" heartOfGold = "repository:read,modify,delete:hof"
puzzle42 = "repository:read,write:p42" puzzle42 = "repository:read,write:p42"
oss = "repository:pull" oss = "repository:pull"