mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 15:35:49 +01:00
Permission Overview
Adds an overview of the permissions of a user including its groups. To do so, a new cache is introduced that stores the groups of a user, when the user is authenticated. In doing so, SCM-Manager can also list groups assigned by external authentication plugins such as LDAP. On the other hand, the user has to have been logged in at least once to get external groups, and even then the cached groups may be out of date when the overview is created. Internal groups will always be added correctly, nonetheless. Due to the cache, another problem arised: On some logins, the xml dao for the cache failed to be read, because it was read and written at the same time. To fix this, a more thorough synchronization of the stores has been implemented. Committed-by: Konstantin Schaper <konstantin.schaper@cloudogu.com> Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
committed by
SCM-Manager
parent
864ed9072d
commit
e1b107849e
BIN
docs/de/user/user/assets/user-permission-overview.png
Normal file
BIN
docs/de/user/user/assets/user-permission-overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -25,3 +25,19 @@ Die Detailseite eines Benutzers zeigt die Informationen zu diesem an.
|
|||||||
Die Checkbox `Extern` zeigt an, ob es sich um einen internen Benutzer handelt oder der Benutzer von einem Fremdsystem verwaltet wird.
|
Die Checkbox `Extern` zeigt an, ob es sich um einen internen Benutzer handelt oder der Benutzer von einem Fremdsystem verwaltet wird.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Berechtigungsübersicht
|
||||||
|
Am unteren Ende der Detailseite kann für einen Benutzer eine Berechtigunsübersicht geladen werden.
|
||||||
|
Diese Übersicht listet alle Gruppen, denen der Benutzer im SCM-Manager zugewiesen ist.
|
||||||
|
Hat sich der Benutzer bereits mindestens einmal angemeldet, so werden darüber hinaus auch alle
|
||||||
|
Gruppen berücksichtigt, die durch externe Berechtigungssysteme (wie z. B. LDAP oder CAS) mitgegeben
|
||||||
|
wurden. Gruppen mit konfigurierten Berechtigungen sind mit einem Haken markiert.
|
||||||
|
Externe Gruppen, die im SCM-Manager noch nicht angelegt sind, können separat gelistet werden.
|
||||||
|
|
||||||
|
Darunter werden alle Namespaces und Repositories gelistet, bei denen für den Benutzer oder
|
||||||
|
eine seiner Gruppen eine Berechtigung konfiguriert ist.
|
||||||
|
|
||||||
|
Die einzelnen Einstellungsseiten für die Berechtigungen können direkt über die Stifte angesprungen
|
||||||
|
werden. Bei bisher noch nicht bekannten Gruppen können diese direkt erstellt werden.
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
BIN
docs/en/user/user/assets/user-permission-overview.png
Normal file
BIN
docs/en/user/user/assets/user-permission-overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -22,3 +22,20 @@ The user details page shows the information about the user.
|
|||||||
The active box shows whether the user is able to use SCM-Manager. The external box shows if it is an internal user or whether it is managed by an external system.
|
The active box shows whether the user is able to use SCM-Manager. The external box shows if it is an internal user or whether it is managed by an external system.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Permission Overview
|
||||||
|
At the bottom of the detail page, a permission overview can be opened.
|
||||||
|
|
||||||
|
This overview lists all groups, the user has been assigned to in SCM-Manager. If the user has
|
||||||
|
been logged in at least once, also groups assigned by external authorization systems (like LDAP or CAS)
|
||||||
|
will be listed. Groups with configured permissions are marked with a checkmark.
|
||||||
|
External groups that have not been created in SCM-Manager can be seen in an extra table.
|
||||||
|
|
||||||
|
Below, all namespaces and repositories are listed, for whom permissions for the user or any of its groups
|
||||||
|
have been configured.
|
||||||
|
|
||||||
|
The single permission configurations can be accessed directly using the edit icons. Currently unknown
|
||||||
|
groups can be created directly.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|||||||
@@ -31,4 +31,13 @@ public interface GroupCollector {
|
|||||||
String AUTHENTICATED = "_authenticated";
|
String AUTHENTICATED = "_authenticated";
|
||||||
|
|
||||||
Set<String> collect(String principal);
|
Set<String> collect(String principal);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the groups of the user that had been assigned at the last login (including all
|
||||||
|
* external groups) and the current internal groups associated to the user. If the
|
||||||
|
* user had not logged in before, only the current internal groups will be returned.
|
||||||
|
*
|
||||||
|
* @since 2.42.0
|
||||||
|
*/
|
||||||
|
Set<String> fromLastLoginPlusInternal(String principal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import sonia.scm.Manager;
|
|||||||
import sonia.scm.search.Searchable;
|
import sonia.scm.search.Searchable;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -51,5 +52,12 @@ public interface GroupManager
|
|||||||
*
|
*
|
||||||
* @return all groups assigned to the given member
|
* @return all groups assigned to the given member
|
||||||
*/
|
*/
|
||||||
public Collection<Group> getGroupsForMember(String member);
|
Collection<Group> getGroupsForMember(String member);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link Set} of all group names.
|
||||||
|
*
|
||||||
|
* @since 2.42.0
|
||||||
|
*/
|
||||||
|
Set<String> getAllNames();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import sonia.scm.ManagerDecorator;
|
|||||||
import sonia.scm.search.SearchRequest;
|
import sonia.scm.search.SearchRequest;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -100,7 +101,10 @@ public class GroupManagerDecorator
|
|||||||
return decorated.getGroupsForMember(member);
|
return decorated.getGroupsForMember(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
@Override
|
||||||
|
public Set<String> getAllNames() {
|
||||||
|
return decorated.getAllNames();
|
||||||
|
}
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private final GroupManager decorated;
|
private final GroupManager decorated;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.xml;
|
||||||
|
|
||||||
|
import sonia.scm.util.Util;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class XmlMapMultiStringAdapter
|
||||||
|
extends XmlAdapter<XmlMapMultiStringElement[], Map<String, Set<String>>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmlMapMultiStringElement[] marshal(Map<String, Set<String>> map) throws Exception {
|
||||||
|
XmlMapMultiStringElement[] elements;
|
||||||
|
|
||||||
|
if (Util.isNotEmpty(map)) {
|
||||||
|
int i = 0;
|
||||||
|
int s = map.size();
|
||||||
|
|
||||||
|
elements = new XmlMapMultiStringElement[s];
|
||||||
|
|
||||||
|
for (Map.Entry<String, Set<String>> e : map.entrySet()) {
|
||||||
|
elements[i] = new XmlMapMultiStringElement(e.getKey(), e.getValue());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements = new XmlMapMultiStringElement[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Set<String>> unmarshal(XmlMapMultiStringElement[] elements)
|
||||||
|
throws Exception
|
||||||
|
{
|
||||||
|
Map<String, Set<String>> map = new HashMap<>();
|
||||||
|
|
||||||
|
if (elements != null)
|
||||||
|
{
|
||||||
|
for (XmlMapMultiStringElement e : elements)
|
||||||
|
{
|
||||||
|
map.put(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.xml;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@XmlRootElement(name = "element")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class XmlMapMultiStringElement {
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
@XmlJavaTypeAdapter(XmlSetStringAdapter.class)
|
||||||
|
private Set<String> value;
|
||||||
|
|
||||||
|
public XmlMapMultiStringElement() {}
|
||||||
|
|
||||||
|
public XmlMapMultiStringElement(String key, Set<String> value) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(Set<String> value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ import javax.xml.bind.JAXBException;
|
|||||||
import javax.xml.bind.Marshaller;
|
import javax.xml.bind.Marshaller;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static sonia.scm.store.CopyOnWrite.compute;
|
||||||
|
|
||||||
public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class);
|
private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class);
|
||||||
@@ -54,6 +56,7 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
|||||||
|
|
||||||
public Repository read(Path path) {
|
public Repository read(Path path) {
|
||||||
LOG.trace("read repository metadata from {}", path);
|
LOG.trace("read repository metadata from {}", path);
|
||||||
|
return compute(() -> {
|
||||||
try {
|
try {
|
||||||
return (Repository) jaxbContext.createUnmarshaller().unmarshal(resolveDataPath(path).toFile());
|
return (Repository) jaxbContext.createUnmarshaller().unmarshal(resolveDataPath(path).toFile());
|
||||||
} catch (JAXBException ex) {
|
} catch (JAXBException ex) {
|
||||||
@@ -61,6 +64,7 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
|||||||
ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex
|
ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}).withLockedFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(Path path, Repository repository) {
|
void write(Path path, Repository repository) {
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static sonia.scm.store.CopyOnWrite.execute;
|
||||||
|
|
||||||
class PathDatabase {
|
class PathDatabase {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(PathDatabase.class);
|
private static final Logger LOG = LoggerFactory.getLogger(PathDatabase.class);
|
||||||
@@ -122,6 +124,7 @@ class PathDatabase {
|
|||||||
|
|
||||||
void read(OnRepositories onRepositories, OnRepository onRepository) {
|
void read(OnRepositories onRepositories, OnRepository onRepository) {
|
||||||
LOG.trace("read repository path database from {}", storePath);
|
LOG.trace("read repository path database from {}", storePath);
|
||||||
|
execute(() -> {
|
||||||
try (AutoCloseableXMLReader reader = XmlStreams.createReader(storePath)) {
|
try (AutoCloseableXMLReader reader = XmlStreams.createReader(storePath)) {
|
||||||
|
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
@@ -143,6 +146,7 @@ class PathDatabase {
|
|||||||
ex
|
ex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}).withLockedFile(storePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readRepository(XMLStreamReader reader, OnRepository onRepository) throws XMLStreamException {
|
private void readRepository(XMLStreamReader reader, OnRepository onRepository) throws XMLStreamException {
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ import com.google.common.util.concurrent.Striped;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CopyOnWrite creates a copy of the target file, before it is modified. This should prevent empty or incomplete files
|
* CopyOnWrite creates a copy of the target file, before it is modified. This should prevent empty or incomplete files
|
||||||
@@ -46,23 +48,56 @@ public final class CopyOnWrite {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class);
|
||||||
|
|
||||||
private static final Striped<Lock> concurrencyLock = Striped.lock(10);
|
private static final Striped<Lock> concurrencyLock = Striped.lock(20);
|
||||||
|
|
||||||
private CopyOnWrite() {
|
private CopyOnWrite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void withTemporaryFile(FileWriter writer, Path targetFile) {
|
public static void withTemporaryFile(FileWriter writer, Path targetFile) {
|
||||||
validateInput(targetFile);
|
validateInput(targetFile);
|
||||||
Lock lock = concurrencyLock.get(targetFile.toString());
|
execute(() -> {
|
||||||
try {
|
|
||||||
lock.lock();
|
|
||||||
Path temporaryFile = createTemporaryFile(targetFile);
|
Path temporaryFile = createTemporaryFile(targetFile);
|
||||||
executeCallback(writer, targetFile, temporaryFile);
|
executeCallback(writer, targetFile, temporaryFile);
|
||||||
replaceOriginalFile(targetFile, temporaryFile);
|
replaceOriginalFile(targetFile, temporaryFile);
|
||||||
|
}).withLockedFile(targetFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <R> FileLocker<R> compute(Supplier<R> supplier) {
|
||||||
|
return new FileLocker<>(supplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FileLocker<Void> execute(Runnable runnable) {
|
||||||
|
return new FileLocker<>(() -> {
|
||||||
|
runnable.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FileLocker<R> {
|
||||||
|
private final Supplier<R> supplier;
|
||||||
|
|
||||||
|
public FileLocker(Supplier<R> supplier) {
|
||||||
|
this.supplier = supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public R withLockedFile(Path file) {
|
||||||
|
return withLockedFile(file.toAbsolutePath().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public R withLockedFile(File file) {
|
||||||
|
return withLockedFile(file.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public R withLockedFile(String file) {
|
||||||
|
Lock lock = concurrencyLock.get(file);
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
return supplier.get();
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("squid:S3725") // performance of Files#isDirectory
|
@SuppressWarnings("squid:S3725") // performance of Files#isDirectory
|
||||||
private static void validateInput(Path targetFile) {
|
private static void validateInput(Path targetFile) {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import java.util.Map.Entry;
|
|||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
|
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
|
||||||
|
import static sonia.scm.store.CopyOnWrite.execute;
|
||||||
|
|
||||||
public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V> {
|
public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V> {
|
||||||
|
|
||||||
@@ -70,19 +71,21 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
// initial load
|
// initial load
|
||||||
|
execute(() -> {
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
}).withLockedFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
LOG.debug("clear configuration store");
|
LOG.debug("clear configuration store");
|
||||||
|
|
||||||
synchronized (file) {
|
execute(() -> {
|
||||||
entries.clear();
|
entries.clear();
|
||||||
store();
|
store();
|
||||||
}
|
}).withLockedFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -98,20 +101,20 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
|
|||||||
public void put(String id, V item) {
|
public void put(String id, V item) {
|
||||||
LOG.debug("put item {} to configuration store", id);
|
LOG.debug("put item {} to configuration store", id);
|
||||||
|
|
||||||
synchronized (file) {
|
execute(() -> {
|
||||||
entries.put(id, item);
|
entries.put(id, item);
|
||||||
store();
|
store();
|
||||||
}
|
}).withLockedFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void remove(String id) {
|
public void remove(String id) {
|
||||||
LOG.debug("remove item {} from configuration store", id);
|
LOG.debug("remove item {} from configuration store", id);
|
||||||
|
|
||||||
synchronized (file) {
|
execute(() -> {
|
||||||
entries.remove(id);
|
entries.remove(id);
|
||||||
store();
|
store();
|
||||||
}
|
}).withLockedFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -135,7 +138,7 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
|
|||||||
|
|
||||||
private void load() {
|
private void load() {
|
||||||
LOG.debug("load configuration from {}", file);
|
LOG.debug("load configuration from {}", file);
|
||||||
|
execute(() ->
|
||||||
context.withUnmarshaller(u -> {
|
context.withUnmarshaller(u -> {
|
||||||
try (AutoCloseableXMLReader reader = XmlStreams.createReader(file)) {
|
try (AutoCloseableXMLReader reader = XmlStreams.createReader(file)) {
|
||||||
|
|
||||||
@@ -175,7 +178,7 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})).withLockedFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void store() {
|
private void store() {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import static sonia.scm.store.CopyOnWrite.compute;
|
||||||
|
import static sonia.scm.store.CopyOnWrite.execute;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JAXB implementation of {@link ConfigurationStore}.
|
* JAXB implementation of {@link ConfigurationStore}.
|
||||||
*
|
*
|
||||||
@@ -69,11 +72,15 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
|
|||||||
protected T readObject() {
|
protected T readObject() {
|
||||||
LOG.debug("load {} from store {}", type, configFile);
|
LOG.debug("load {} from store {}", type, configFile);
|
||||||
|
|
||||||
|
return compute(
|
||||||
|
() -> {
|
||||||
if (configFile.exists()) {
|
if (configFile.exists()) {
|
||||||
return context.unmarshall(configFile);
|
return context.unmarshall(configFile);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
).withLockedFile(configFile);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void writeObject(T object) {
|
protected void writeObject(T object) {
|
||||||
@@ -87,10 +94,12 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
|
|||||||
@Override
|
@Override
|
||||||
protected void deleteObject() {
|
protected void deleteObject() {
|
||||||
LOG.debug("deletes {}", configFile.getPath());
|
LOG.debug("deletes {}", configFile.getPath());
|
||||||
|
execute(() -> {
|
||||||
try {
|
try {
|
||||||
IOUtil.delete(configFile);
|
IOUtil.delete(configFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new StoreException("Failed to delete store object " + configFile.getPath(), e);
|
throw new StoreException("Failed to delete store object " + configFile.getPath(), e);
|
||||||
}
|
}
|
||||||
|
}).withLockedFile(configFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import javax.xml.bind.Marshaller;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static sonia.scm.store.CopyOnWrite.compute;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jaxb implementation of {@link DataStore}.
|
* Jaxb implementation of {@link DataStore}.
|
||||||
*
|
*
|
||||||
@@ -106,10 +108,12 @@ public class JAXBDataStore<T> extends FileBasedStore<T> implements DataStore<T>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected T read(File file) {
|
protected T read(File file) {
|
||||||
|
return compute(() -> {
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
LOG.trace("try to read {}", file);
|
LOG.trace("try to read {}", file);
|
||||||
return context.unmarshall(file);
|
return context.unmarshall(file);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
}).withLockedFile(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
import { ApiResult, useRequiredIndexLink } from "./base";
|
import { ApiResult, useRequiredIndexLink } from "./base";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import { Link, Me, User, UserCollection, UserCreation } from "@scm-manager/ui-types";
|
import { Link, Me, PermissionOverview, User, UserCollection, UserCreation } from "@scm-manager/ui-types";
|
||||||
import { apiClient } from "./apiclient";
|
import { apiClient } from "./apiclient";
|
||||||
import { createQueryString } from "./utils";
|
import { createQueryString } from "./utils";
|
||||||
import { concat } from "./urls";
|
import { concat } from "./urls";
|
||||||
@@ -65,6 +65,13 @@ export const useUser = (name: string): ApiResult<User> => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUserPermissionOverview = (user: User): ApiResult<PermissionOverview> => {
|
||||||
|
const overviewLink = user._links.permissionOverview as Link;
|
||||||
|
return useQuery<PermissionOverview, Error>(["user", user.name, "permissionOverview"], () =>
|
||||||
|
apiClient.get(overviewLink.href).then((response) => response.json())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const createUser = (link: string) => {
|
const createUser = (link: string) => {
|
||||||
return (user: UserCreation) => {
|
return (user: UserCreation) => {
|
||||||
return apiClient
|
return apiClient
|
||||||
|
|||||||
@@ -48,3 +48,20 @@ export type UserCreation = User;
|
|||||||
export type UserCollection = PagedCollection<{
|
export type UserCollection = PagedCollection<{
|
||||||
users: User[];
|
users: User[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type PermissionOverview = HalRepresentation & {
|
||||||
|
relevantGroups: PermissionOverviewGroupEntry[];
|
||||||
|
relevantNamespaces: string[];
|
||||||
|
relevantRepositories: PermissionOverviewRepositoryEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionOverviewGroupEntry = {
|
||||||
|
name: string;
|
||||||
|
permissions: boolean;
|
||||||
|
externalOnly: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionOverviewRepositoryEntry = {
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -30,9 +30,6 @@
|
|||||||
"noUsers": "Keine Benutzer gefunden.",
|
"noUsers": "Keine Benutzer gefunden.",
|
||||||
"createButton": "Benutzer erstellen"
|
"createButton": "Benutzer erstellen"
|
||||||
},
|
},
|
||||||
"overview": {
|
|
||||||
"filterUser": "Benutzer filtern"
|
|
||||||
},
|
|
||||||
"singleUser": {
|
"singleUser": {
|
||||||
"errorTitle": "Fehler",
|
"errorTitle": "Fehler",
|
||||||
"errorSubtitle": "Unbekannter Benutzer Fehler",
|
"errorSubtitle": "Unbekannter Benutzer Fehler",
|
||||||
@@ -47,6 +44,9 @@
|
|||||||
"setApiKeyNavLink": "API Schlüssel"
|
"setApiKeyNavLink": "API Schlüssel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"overview": {
|
||||||
|
"filterUser": "Benutzer filtern"
|
||||||
|
},
|
||||||
"createUser": {
|
"createUser": {
|
||||||
"title": "Benutzer erstellen",
|
"title": "Benutzer erstellen",
|
||||||
"subtitle": "Erstellen eines neuen Benutzers",
|
"subtitle": "Erstellen eines neuen Benutzers",
|
||||||
@@ -172,5 +172,27 @@
|
|||||||
"submit": "Ja",
|
"submit": "Ja",
|
||||||
"cancel": "Nein"
|
"cancel": "Nein"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"permissionOverview": {
|
||||||
|
"title": "Berechtigungsübersicht",
|
||||||
|
"help": "Nach einer Anmeldung dieses Kontos basiert diese Übersicht auf den Gruppen, die diesem Konto bei der letzten Anmeldung zugeordnet wurden. Daher beinhaltet sie auch solche Gruppen, die nicht im SCM-Manager eingerichtet sind, sondern von externen Systemen bereitgestellt wurden (wie z. B. LDAP). Da solche externen Daten auf der letzten Anmeldung basieren, stellt diese Übersicht unter Umständen nicht den aktuellen Stand dar. Gab es noch keine Anmeldung mit diesem Konto, so werden nur die intern zugewiesenen Gruppen aufgelistet.",
|
||||||
|
"groups": {
|
||||||
|
"noRepositoriesFound": "Keine Namespaces oder Repositories mit Berechtigungen vorhanden.",
|
||||||
|
"showGroupsWithoutPermission": "Noch nicht in SCM-Manager angelegte fremde Gruppen anzeigen",
|
||||||
|
"showGroupsWithoutPermissionHelp": "Diese Option zeigt fremde Gruppen an. Dies sind Gruppen, die im SCM-Manager nicht bekannt sind, sondern von externen Systemen stammen. Um diese Gruppen zu berechtigen, müssen sie zunächst im SCM-Manager (als externe Gruppen) angelegt werden.",
|
||||||
|
"noGroupsFound": "Keine Gruppen vorhanden.",
|
||||||
|
"noUnknownGroupsFound": "Keine weiteren Gruppen vorhanden.",
|
||||||
|
"groupName": "Name",
|
||||||
|
"permissionsConfigured": "Berechtigungen gesetzt",
|
||||||
|
"createGroup": "Erstellen",
|
||||||
|
"editPermissions": "Bearbeiten"
|
||||||
|
},
|
||||||
|
"repositories": {
|
||||||
|
"subtitle": "Namespaces / Repositories",
|
||||||
|
"namespaceName": "Namespace / Name",
|
||||||
|
"permissionsConfigured": "Berechtigungen gesetzt",
|
||||||
|
"editPermissions": "Bearbeiten"
|
||||||
|
},
|
||||||
|
"edit": "bearbeiten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,5 +172,27 @@
|
|||||||
"submit": "Yes",
|
"submit": "Yes",
|
||||||
"cancel": "No"
|
"cancel": "No"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"permissionOverview": {
|
||||||
|
"title": "Permission Overview",
|
||||||
|
"help": "If the user has logged in at leas once, this overview is based on the groups assigned to the user at the last login. Therefor this also includes groups, that have not been set up in SCM-Manager but were provided by external systems (such as LDAP). Because this view is based on the last login of the user, this might not reflect the current state of the external groups if the user would log in now. If the user has never logged in before, only the internal groups for this user will be listed.",
|
||||||
|
"groups": {
|
||||||
|
"noRepositoriesFound": "No namespaces/repositories available.",
|
||||||
|
"showGroupsWithoutPermission": "Show external groups not yet created in SCM-Manager",
|
||||||
|
"showGroupsWithoutPermissionHelp": "This option shows foreign groups. These are groups, that have come from external systems and are unknown to SCM-Manager. The assign permissions for these groups, they have to be created in SCM-Manager first (as external groups).",
|
||||||
|
"noGroupsFound": "No groups available.",
|
||||||
|
"noUnknownGroupsFound": "No further groups available.",
|
||||||
|
"groupName": "Name",
|
||||||
|
"permissionsConfigured": "Permissions set",
|
||||||
|
"createGroup": "Create",
|
||||||
|
"editPermissions": "Edit"
|
||||||
|
},
|
||||||
|
"repositories": {
|
||||||
|
"subtitle": "Namespaces / Repositories",
|
||||||
|
"namespaceName": "Namespace / Name",
|
||||||
|
"permissionsConfigured": "Permissions set",
|
||||||
|
"editPermissions": "Edit"
|
||||||
|
},
|
||||||
|
"edit": "edit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ type Props = {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
group?: Group;
|
group?: Group;
|
||||||
loadUserSuggestions: (p: string) => Promise<SelectValue[]>;
|
loadUserSuggestions: (p: string) => Promise<SelectValue[]>;
|
||||||
|
transmittedName?: string;
|
||||||
|
transmittedExternal?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupForm: FC<Props> = ({ submitForm, loading, group, loadUserSuggestions }) => {
|
const GroupForm: FC<Props> = ({ submitForm, loading, group, loadUserSuggestions, transmittedName = "", transmittedExternal = false }) => {
|
||||||
const [t] = useTranslation("groups");
|
const [t] = useTranslation("groups");
|
||||||
const [groupState, setGroupState] = useState({
|
const [groupState, setGroupState] = useState({
|
||||||
name: "",
|
name: transmittedName,
|
||||||
description: "",
|
description: "",
|
||||||
_embedded: {
|
_embedded: {
|
||||||
members: [] as Member[]
|
members: [] as Member[]
|
||||||
@@ -54,7 +56,7 @@ const GroupForm: FC<Props> = ({ submitForm, loading, group, loadUserSuggestions
|
|||||||
_links: {},
|
_links: {},
|
||||||
members: [] as string[],
|
members: [] as string[],
|
||||||
type: "",
|
type: "",
|
||||||
external: false
|
external: transmittedExternal
|
||||||
});
|
});
|
||||||
const [nameValidationError, setNameValidationError] = useState(false);
|
const [nameValidationError, setNameValidationError] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect, useLocation } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useCreateGroup, useUserSuggestions } from "@scm-manager/ui-api";
|
import { useCreateGroup, useUserSuggestions, urls } from "@scm-manager/ui-api";
|
||||||
import { Page } from "@scm-manager/ui-components";
|
import { Page } from "@scm-manager/ui-components";
|
||||||
import GroupForm from "../components/GroupForm";
|
import GroupForm from "../components/GroupForm";
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ const CreateGroup: FC = () => {
|
|||||||
const [t] = useTranslation("groups");
|
const [t] = useTranslation("groups");
|
||||||
const { isLoading, create, error, group } = useCreateGroup();
|
const { isLoading, create, error, group } = useCreateGroup();
|
||||||
const userSuggestions = useUserSuggestions();
|
const userSuggestions = useUserSuggestions();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
if (group) {
|
if (group) {
|
||||||
return <Redirect to={`/group/${group.name}`} />;
|
return <Redirect to={`/group/${group.name}`} />;
|
||||||
@@ -40,7 +41,13 @@ const CreateGroup: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Page title={t("addGroup.title")} subtitle={t("addGroup.subtitle")} error={error || undefined}>
|
<Page title={t("addGroup.title")} subtitle={t("addGroup.subtitle")} error={error || undefined}>
|
||||||
<div>
|
<div>
|
||||||
<GroupForm submitForm={create} loading={isLoading} loadUserSuggestions={userSuggestions} />
|
<GroupForm
|
||||||
|
submitForm={create}
|
||||||
|
loading={isLoading}
|
||||||
|
loadUserSuggestions={userSuggestions}
|
||||||
|
transmittedName={urls.getValueStringFromLocationByKey(location, "name")}
|
||||||
|
transmittedExternal={urls.getValueStringFromLocationByKey(location, "external") === "true"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
295
scm-ui/ui-webapp/src/users/components/PermissionOverview.tsx
Normal file
295
scm-ui/ui-webapp/src/users/components/PermissionOverview.tsx
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { Checkbox, ErrorNotification, Icon, Loading, Notification } from "@scm-manager/ui-components";
|
||||||
|
import {
|
||||||
|
Group,
|
||||||
|
Link as HalLink,
|
||||||
|
Links,
|
||||||
|
Namespace,
|
||||||
|
PermissionOverview as Data,
|
||||||
|
PermissionOverviewGroupEntry,
|
||||||
|
Repository,
|
||||||
|
User,
|
||||||
|
} from "@scm-manager/ui-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useUserPermissionOverview } from "@scm-manager/ui-api";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const NamespaceColumn = styled.th`
|
||||||
|
width: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EditIcon: FC = () => {
|
||||||
|
const [t] = useTranslation("users");
|
||||||
|
return <Icon alt={t("permissionOverview.edit")} name="pen" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditLink: FC<{ links?: Links; to: string }> = ({ links, to }) => {
|
||||||
|
if (!links?.permissions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link to={to}>
|
||||||
|
<EditIcon />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ElementLink: FC<{ link?: HalLink; to: string }> = ({ link, to, children }) => {
|
||||||
|
if (!link) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
return <Link to={to}>{children}</Link>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GroupRow: FC<{ entry: PermissionOverviewGroupEntry; group?: Group }> = ({ entry, group }) => (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<ElementLink to={`/group/${entry.name}/`} link={group?._links?.self as HalLink}>
|
||||||
|
{entry.name}
|
||||||
|
</ElementLink>
|
||||||
|
</td>
|
||||||
|
<td align="center">{entry.permissions && <Icon name="check" />}</td>
|
||||||
|
<td align="center">
|
||||||
|
<EditLink to={`/group/${entry.name}/settings/permissions`} links={group?._links} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NotCreatedGroupRow: FC<{ entry: PermissionOverviewGroupEntry }> = ({ entry }) => (
|
||||||
|
<tr>
|
||||||
|
<td>{entry.name}</td>
|
||||||
|
<td align="center">
|
||||||
|
<Link to={`/groups/create/?name=${entry.name}&external=true`}>
|
||||||
|
<EditIcon />
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RepositoryNamespaceRows: FC<{
|
||||||
|
entry: { namespace: Namespace; repositories: Repository[] };
|
||||||
|
relevant: boolean;
|
||||||
|
}> = ({ entry, relevant }) => (
|
||||||
|
<>
|
||||||
|
<NamespaceRow key={entry.namespace.namespace} namespace={entry.namespace} relevant={relevant} />
|
||||||
|
{entry.repositories.map((repository) => (
|
||||||
|
<RepositoryRow key={`${repository.namespace}/${repository.name}`} entry={repository} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NamespaceRow: FC<{ namespace: Namespace; relevant: boolean }> = ({ namespace, relevant }) => (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<Link to={`/repos/${namespace.namespace}/`}>{namespace.namespace}</Link>
|
||||||
|
</td>
|
||||||
|
<td align="center">{relevant && <Icon name="check" />}</td>
|
||||||
|
<td align="center">
|
||||||
|
<EditLink links={namespace._links} to={`/namespace/${namespace.namespace}/settings/permissions`} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RepositoryRow: FC<{ entry: Repository }> = ({ entry }) => (
|
||||||
|
<tr>
|
||||||
|
<td />
|
||||||
|
<td>
|
||||||
|
<Link to={`/repo/${entry.namespace}/${entry.name}/`}>{entry.name}</Link>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<Icon name="check" />
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<EditLink links={entry._links} to={`/repo/${entry.namespace}/${entry.name}/settings/permissions`} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
const GroupTable: FC<{ data: Data }> = ({ data }) => {
|
||||||
|
const [t] = useTranslation("users");
|
||||||
|
|
||||||
|
if (data.relevantGroups.find((entry) => !entry.externalOnly)) {
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("permissionOverview.groups.groupName")}</th>
|
||||||
|
<th align="center">{t("permissionOverview.groups.permissionsConfigured")}</th>
|
||||||
|
<th align="center">{t("permissionOverview.groups.editPermissions")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.relevantGroups
|
||||||
|
.filter((entry) => !entry.externalOnly)
|
||||||
|
.map((entry) => (
|
||||||
|
<GroupRow
|
||||||
|
key={entry.name}
|
||||||
|
entry={entry}
|
||||||
|
group={(data._embedded?.groups as Group[]).find((group) => group.name === entry.name)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <Notification type="info">{t("permissionOverview.groups.noGroupsFound")}</Notification>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const GroupsWithoutPermissionTable: FC<{ data: Data }> = ({ data }) => {
|
||||||
|
const [t] = useTranslation("users");
|
||||||
|
const [external, setExternal] = useState(false);
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (!external) {
|
||||||
|
content = null;
|
||||||
|
} else if (data.relevantGroups.find((entry) => entry.externalOnly)) {
|
||||||
|
content = (
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("permissionOverview.groups.groupName")}</th>
|
||||||
|
<th align="center">{t("permissionOverview.groups.createGroup")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.relevantGroups
|
||||||
|
.filter((entry) => entry.externalOnly)
|
||||||
|
.map((entry) => (
|
||||||
|
<NotCreatedGroupRow key={entry.name} entry={entry} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = <Notification type="info">{t("permissionOverview.groups.noUnknownGroupsFound")}</Notification>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Checkbox
|
||||||
|
label={t("permissionOverview.groups.showGroupsWithoutPermission")}
|
||||||
|
onChange={setExternal}
|
||||||
|
helpText={t("permissionOverview.groups.showGroupsWithoutPermissionHelp")}
|
||||||
|
/>
|
||||||
|
{content}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RepositoryTable: FC<{ data: Data }> = ({ data }) => {
|
||||||
|
const [t] = useTranslation("users");
|
||||||
|
|
||||||
|
if ((!data.relevantNamespaces || data.relevantNamespaces.length === 0) && !data.relevantRepositories) {
|
||||||
|
return <Notification type="info">{t("permissionOverview.groups.noRepositoriesFound")}</Notification>;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.relevantRepositories.sort((r1, r2) =>
|
||||||
|
r1.namespace === r2.namespace ? (r1.name < r2.name ? -1 : +1) : r1.namespace < r2.namespace ? -1 : +1
|
||||||
|
);
|
||||||
|
|
||||||
|
const findRelevantNamespace = (namespace: string) =>
|
||||||
|
(data._embedded?.relevantNamespaces as Namespace[]).find((n: Namespace) => n.namespace === namespace);
|
||||||
|
const findOtherNamespace = (namespace: string) =>
|
||||||
|
(data._embedded?.otherNamespaces as Namespace[]).find((n: Namespace) => n.namespace === namespace);
|
||||||
|
|
||||||
|
const allNamespaces = new Set<string>();
|
||||||
|
data.relevantRepositories.forEach((repo) => allNamespaces.add(repo.namespace));
|
||||||
|
data.relevantNamespaces.forEach((namespace) => allNamespaces.add(namespace));
|
||||||
|
const sortedNamespaces: string[] = Array.from(allNamespaces).sort();
|
||||||
|
|
||||||
|
const repositoriesForNamespace = (namespace: string) =>
|
||||||
|
data.relevantRepositories
|
||||||
|
.filter((repo) => repo.namespace === namespace)
|
||||||
|
.map(
|
||||||
|
(repo) =>
|
||||||
|
(data._embedded?.repositories as Repository[]).find(
|
||||||
|
(r: Repository) => r.namespace === repo.namespace && r.name === repo.name
|
||||||
|
) || ({ ...repo, _links: {} } as Repository)
|
||||||
|
);
|
||||||
|
|
||||||
|
const reposInNamespaces = sortedNamespaces.map((namespace) => {
|
||||||
|
return {
|
||||||
|
namespace: findRelevantNamespace(namespace) ||
|
||||||
|
findOtherNamespace(namespace) || { namespace: namespace, _links: {} },
|
||||||
|
repositories: repositoriesForNamespace(namespace),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<NamespaceColumn colSpan={2}>{t("permissionOverview.repositories.namespaceName")}</NamespaceColumn>
|
||||||
|
<th align="center">{t("permissionOverview.repositories.permissionsConfigured")}</th>
|
||||||
|
<th align="center">{t("permissionOverview.repositories.editPermissions")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{reposInNamespaces.map((entry) => (
|
||||||
|
<RepositoryNamespaceRows
|
||||||
|
key={entry.namespace.namespace}
|
||||||
|
entry={entry}
|
||||||
|
relevant={!!findRelevantNamespace(entry.namespace.namespace)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PermissionOverview: FC<{ user: User }> = ({ user }) => {
|
||||||
|
const { data, isLoading, error } = useUserPermissionOverview(user);
|
||||||
|
const [t] = useTranslation("users");
|
||||||
|
|
||||||
|
if (isLoading || !data) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorNotification error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To test the table with the "not created" groups, you can mock such data
|
||||||
|
// with the following statement and assign this in the GroupsWithoutPermissionTable:
|
||||||
|
// const mockedData = {
|
||||||
|
// ...data,
|
||||||
|
// relevantGroups: [...data.relevantGroups, { name: "hitchhiker", permissions: false, externalOnly: true }],
|
||||||
|
// };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GroupTable data={data} />
|
||||||
|
<GroupsWithoutPermissionTable data={data} />
|
||||||
|
<h4>{t("permissionOverview.repositories.subtitle")}</h4>
|
||||||
|
<RepositoryTable data={data} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PermissionOverview;
|
||||||
@@ -21,19 +21,47 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { useTranslation, WithTranslation } from "react-i18next";
|
||||||
import { User } from "@scm-manager/ui-types";
|
import { User } from "@scm-manager/ui-types";
|
||||||
import { Checkbox, createAttributesForTesting, DateFromNow, InfoTable, MailLink } from "@scm-manager/ui-components";
|
import {
|
||||||
|
Checkbox,
|
||||||
|
createAttributesForTesting,
|
||||||
|
DateFromNow,
|
||||||
|
Help,
|
||||||
|
InfoTable,
|
||||||
|
MailLink
|
||||||
|
} from "@scm-manager/ui-components";
|
||||||
|
import { Icon } from "@scm-manager/ui-components";
|
||||||
|
import PermissionOverview from "../PermissionOverview";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Details extends React.Component<Props> {
|
const Details: FC<Props> = ({ user }) => {
|
||||||
render() {
|
const [t] = useTranslation("users");
|
||||||
const { user, t } = this.props;
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
|
const toggleCollapse = () => setCollapsed(!collapsed);
|
||||||
|
|
||||||
|
let permissionOverview;
|
||||||
|
if (user._links.permissionOverview) {
|
||||||
|
let icon = <Icon name="angle-right" color="inherit" alt={t("diff.showContent")} />;
|
||||||
|
if (!collapsed) {
|
||||||
|
icon = <Icon name="angle-down" color="inherit" alt={t("diff.hideContent")} />;
|
||||||
|
}
|
||||||
|
permissionOverview = (
|
||||||
|
<div className="content">
|
||||||
|
<h3 className="is-clickable" onClick={toggleCollapse}>
|
||||||
|
{icon} {t("permissionOverview.title")} <Help message={t("permissionOverview.help")} />
|
||||||
|
</h3>
|
||||||
|
{!collapsed && <PermissionOverview user={user} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<InfoTable>
|
<InfoTable>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -76,8 +104,9 @@ class Details extends React.Component<Props> {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</InfoTable>
|
</InfoTable>
|
||||||
|
{permissionOverview}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation("users")(Details);
|
export default Details;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.Setter;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@SuppressWarnings("java:S2160") // no equals needed in dto
|
||||||
|
class PermissionOverviewDto extends HalRepresentation {
|
||||||
|
|
||||||
|
private Collection<PermissionOverviewDto.GroupEntryDto> relevantGroups;
|
||||||
|
private Collection<String> relevantNamespaces;
|
||||||
|
private Collection<RepositoryEntry> relevantRepositories;
|
||||||
|
|
||||||
|
PermissionOverviewDto(Links links, Embedded embedded) {
|
||||||
|
super(links, embedded);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
static class GroupEntryDto {
|
||||||
|
private String name;
|
||||||
|
private boolean permissions;
|
||||||
|
private boolean externalOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
static class RepositoryEntry {
|
||||||
|
private String namespace;
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.Embedded;
|
||||||
|
import org.mapstruct.Context;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.ObjectFactory;
|
||||||
|
import sonia.scm.group.GroupManager;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.user.PermissionOverview;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.otto.edison.hal.Links.linkingTo;
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
abstract class PermissionOverviewToPermissionOverviewDtoMapper {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ResourceLinks resourceLinks;
|
||||||
|
@Inject
|
||||||
|
private RepositoryToRepositoryDtoMapper repositoryToRepositoryDtoMapper;
|
||||||
|
@Inject
|
||||||
|
private NamespaceToNamespaceDtoMapper namespaceToNamespaceDtoMapper;
|
||||||
|
@Inject
|
||||||
|
private GroupManager groupManager;
|
||||||
|
@Inject
|
||||||
|
private GroupToGroupDtoMapper groupToGroupDtoMapper;
|
||||||
|
|
||||||
|
abstract PermissionOverviewDto toDto(PermissionOverview permissionOverview, @Context String userName);
|
||||||
|
|
||||||
|
abstract PermissionOverviewDto.GroupEntryDto toDto(PermissionOverview.GroupEntry groupEntry);
|
||||||
|
|
||||||
|
abstract PermissionOverviewDto.RepositoryEntry toDto(Repository repository);
|
||||||
|
|
||||||
|
@ObjectFactory
|
||||||
|
PermissionOverviewDto createDto(PermissionOverview permissionOverview, @Context String userName) {
|
||||||
|
List<NamespaceDto> relevantNamespaces = permissionOverview
|
||||||
|
.getRelevantNamespaces()
|
||||||
|
.stream()
|
||||||
|
.map(namespaceToNamespaceDtoMapper::map)
|
||||||
|
.collect(toList());
|
||||||
|
List<NamespaceDto> otherNamespaces = permissionOverview
|
||||||
|
.getRelevantRepositories()
|
||||||
|
.stream()
|
||||||
|
.map(Repository::getNamespace)
|
||||||
|
.distinct()
|
||||||
|
.filter(namespace -> !permissionOverview.getRelevantNamespaces().contains(namespace))
|
||||||
|
.map(namespaceToNamespaceDtoMapper::map)
|
||||||
|
.collect(toList());
|
||||||
|
List<RepositoryDto> repositories = permissionOverview
|
||||||
|
.getRelevantRepositories()
|
||||||
|
.stream()
|
||||||
|
.map(repositoryToRepositoryDtoMapper::map)
|
||||||
|
.collect(toList());
|
||||||
|
List<GroupDto> groups = permissionOverview
|
||||||
|
.getRelevantGroups()
|
||||||
|
.stream()
|
||||||
|
.map(PermissionOverview.GroupEntry::getName)
|
||||||
|
.map(groupManager::get)
|
||||||
|
.map(groupToGroupDtoMapper::map)
|
||||||
|
.collect(toList());
|
||||||
|
Embedded.Builder embedded = new Embedded.Builder()
|
||||||
|
.with("relevantNamespaces", relevantNamespaces)
|
||||||
|
.with("otherNamespaces", otherNamespaces)
|
||||||
|
.with("repositories", repositories)
|
||||||
|
.with("groups", groups);
|
||||||
|
return new PermissionOverviewDto(
|
||||||
|
linkingTo().self(resourceLinks.user().permissionOverview(userName)).build(),
|
||||||
|
embedded.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,6 +148,10 @@ class ResourceLinks {
|
|||||||
return userLinkBuilder.method("getUserResource").parameters(name).method("toInternal").parameters().href();
|
return userLinkBuilder.method("getUserResource").parameters(name).method("toInternal").parameters().href();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String permissionOverview(String name) {
|
||||||
|
return userLinkBuilder.method("getUserResource").parameters(name).method("permissionOverview").parameters().href();
|
||||||
|
}
|
||||||
|
|
||||||
public String publicKeys(String name) {
|
public String publicKeys(String name) {
|
||||||
return publicKeyLinkBuilder.method("findAll").parameters(name).href();
|
return publicKeyLinkBuilder.method("findAll").parameters(name).href();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import org.apache.shiro.authc.credential.PasswordService;
|
import org.apache.shiro.authc.credential.PasswordService;
|
||||||
|
import sonia.scm.user.PermissionOverviewCollector;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
@@ -44,30 +45,36 @@ import javax.ws.rs.PUT;
|
|||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
public class UserResource {
|
public class UserResource {
|
||||||
|
|
||||||
private final UserDtoToUserMapper dtoToUserMapper;
|
private final UserDtoToUserMapper dtoToUserMapper;
|
||||||
private final UserToUserDtoMapper userToDtoMapper;
|
private final UserToUserDtoMapper userToDtoMapper;
|
||||||
|
private final PermissionOverviewToPermissionOverviewDtoMapper permissionOverviewMapper;
|
||||||
|
|
||||||
private final IdResourceManagerAdapter<User, UserDto> adapter;
|
private final IdResourceManagerAdapter<User, UserDto> adapter;
|
||||||
private final UserManager userManager;
|
private final UserManager userManager;
|
||||||
private final PasswordService passwordService;
|
private final PasswordService passwordService;
|
||||||
private final UserPermissionResource userPermissionResource;
|
private final UserPermissionResource userPermissionResource;
|
||||||
|
private final PermissionOverviewCollector permissionOverviewCollector;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UserResource(
|
public UserResource(UserDtoToUserMapper dtoToUserMapper,
|
||||||
UserDtoToUserMapper dtoToUserMapper,
|
|
||||||
UserToUserDtoMapper userToDtoMapper,
|
UserToUserDtoMapper userToDtoMapper,
|
||||||
UserManager manager,
|
PermissionOverviewToPermissionOverviewDtoMapper permissionOverviewMapper, UserManager manager,
|
||||||
PasswordService passwordService, UserPermissionResource userPermissionResource) {
|
PasswordService passwordService,
|
||||||
|
UserPermissionResource userPermissionResource,
|
||||||
|
PermissionOverviewCollector permissionOverviewCollector) {
|
||||||
this.dtoToUserMapper = dtoToUserMapper;
|
this.dtoToUserMapper = dtoToUserMapper;
|
||||||
this.userToDtoMapper = userToDtoMapper;
|
this.userToDtoMapper = userToDtoMapper;
|
||||||
|
this.permissionOverviewMapper = permissionOverviewMapper;
|
||||||
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
|
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
|
||||||
this.userManager = manager;
|
this.userManager = manager;
|
||||||
this.passwordService = passwordService;
|
this.passwordService = passwordService;
|
||||||
this.userPermissionResource = userPermissionResource;
|
this.userPermissionResource = userPermissionResource;
|
||||||
|
this.permissionOverviewCollector = permissionOverviewCollector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -298,6 +305,13 @@ public class UserResource {
|
|||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("permissionOverview")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public PermissionOverviewDto permissionOverview(@PathParam("id") String name) {
|
||||||
|
return permissionOverviewMapper.toDto(permissionOverviewCollector.create(name), name);
|
||||||
|
}
|
||||||
|
|
||||||
@Path("permissions")
|
@Path("permissions")
|
||||||
public UserPermissionResource permissions() {
|
public UserPermissionResource permissions() {
|
||||||
return userPermissionResource;
|
return userPermissionResource;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import de.otto.edison.hal.Links;
|
|||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.Mapping;
|
import org.mapstruct.Mapping;
|
||||||
import org.mapstruct.ObjectFactory;
|
import org.mapstruct.ObjectFactory;
|
||||||
|
import sonia.scm.group.GroupPermissions;
|
||||||
import sonia.scm.security.PermissionPermissions;
|
import sonia.scm.security.PermissionPermissions;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
@@ -76,6 +77,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
|||||||
}
|
}
|
||||||
if (PermissionPermissions.read().isPermitted()) {
|
if (PermissionPermissions.read().isPermitted()) {
|
||||||
linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(user.getName())));
|
linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(user.getName())));
|
||||||
|
if (GroupPermissions.list().isPermitted()) {
|
||||||
|
linksBuilder.single(link("permissionOverview", resourceLinks.user().permissionOverview(user.getName())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||||
|
|||||||
@@ -34,11 +34,15 @@ import sonia.scm.cache.Cache;
|
|||||||
import sonia.scm.cache.CacheManager;
|
import sonia.scm.cache.CacheManager;
|
||||||
import sonia.scm.security.Authentications;
|
import sonia.scm.security.Authentications;
|
||||||
import sonia.scm.security.LogoutEvent;
|
import sonia.scm.security.LogoutEvent;
|
||||||
|
import sonia.scm.store.ConfigurationStore;
|
||||||
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
import sonia.scm.user.UserEvent;
|
import sonia.scm.user.UserEvent;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect groups for a certain principal.
|
* Collect groups for a certain principal.
|
||||||
@@ -56,11 +60,14 @@ public class DefaultGroupCollector implements GroupCollector {
|
|||||||
private final Cache<String, Set<String>> cache;
|
private final Cache<String, Set<String>> cache;
|
||||||
private final Set<GroupResolver> groupResolvers;
|
private final Set<GroupResolver> groupResolvers;
|
||||||
|
|
||||||
|
private final ConfigurationStoreFactory configurationStoreFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set<GroupResolver> groupResolvers) {
|
public DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set<GroupResolver> groupResolvers, ConfigurationStoreFactory configurationStoreFactory) {
|
||||||
this.groupDAO = groupDAO;
|
this.groupDAO = groupDAO;
|
||||||
this.cache = cacheManager.getCache(CACHE_NAME);
|
this.cache = cacheManager.getCache(CACHE_NAME);
|
||||||
this.groupResolvers = groupResolvers;
|
this.groupResolvers = groupResolvers;
|
||||||
|
this.configurationStoreFactory = configurationStoreFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -79,9 +86,30 @@ public class DefaultGroupCollector implements GroupCollector {
|
|||||||
|
|
||||||
Set<String> groups = builder.build();
|
Set<String> groups = builder.build();
|
||||||
LOG.debug("collected following groups for principal {}: {}", principal, groups);
|
LOG.debug("collected following groups for principal {}: {}", principal, groups);
|
||||||
|
|
||||||
|
ConfigurationStore<UserGroupCache> store = createStore();
|
||||||
|
UserGroupCache persistentCache = getPersistentCache(store);
|
||||||
|
persistentCache.put(principal, groups);
|
||||||
|
store.set(persistentCache);
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> fromLastLoginPlusInternal(String principal) {
|
||||||
|
Set<String> cached = new HashSet<>(getPersistentCache(createStore()).get(principal));
|
||||||
|
computeInternalGroups(principal).forEach(cached::add);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserGroupCache getPersistentCache(ConfigurationStore<UserGroupCache> store) {
|
||||||
|
return store.getOptional().orElseGet(UserGroupCache::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigurationStore<UserGroupCache> createStore() {
|
||||||
|
return configurationStoreFactory.withType(UserGroupCache.class).withName("user-group-cache").build();
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe(async = false)
|
@Subscribe(async = false)
|
||||||
public void clearCacheOnLogOut(LogoutEvent event) {
|
public void clearCacheOnLogOut(LogoutEvent event) {
|
||||||
String principal = event.getPrimaryPrincipal();
|
String principal = event.getPrimaryPrincipal();
|
||||||
@@ -95,12 +123,12 @@ public class DefaultGroupCollector implements GroupCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Stream<String> computeInternalGroups(String principal) {
|
||||||
|
return groupDAO.getAll().stream().filter(group -> group.isMember(principal)).map(Group::getName);
|
||||||
|
}
|
||||||
|
|
||||||
private void appendInternalGroups(String principal, ImmutableSet.Builder<String> builder) {
|
private void appendInternalGroups(String principal, ImmutableSet.Builder<String> builder) {
|
||||||
for (Group group : groupDAO.getAll()) {
|
computeInternalGroups(principal).forEach(builder::add);
|
||||||
if (group.isMember(principal)) {
|
|
||||||
builder.add(group.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> resolveExternalGroups(String principal) {
|
private Set<String> resolveExternalGroups(String principal) {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import sonia.scm.HandlerEventType;
|
|||||||
import sonia.scm.ManagerDaoAdapter;
|
import sonia.scm.ManagerDaoAdapter;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.TransformFilter;
|
|
||||||
import sonia.scm.search.SearchRequest;
|
import sonia.scm.search.SearchRequest;
|
||||||
import sonia.scm.search.SearchUtil;
|
import sonia.scm.search.SearchUtil;
|
||||||
import sonia.scm.util.CollectionAppender;
|
import sonia.scm.util.CollectionAppender;
|
||||||
@@ -50,8 +49,11 @@ import java.util.Collections;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.toSet;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -346,7 +348,11 @@ public class DefaultGroupManager extends AbstractGroupManager
|
|||||||
return groupDAO.getLastModified();
|
return groupDAO.getLastModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
@Override
|
||||||
|
public Set<String> getAllNames() {
|
||||||
|
GroupPermissions.list().check();
|
||||||
|
return groupDAO.getAll().stream().map(Group::getName).collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove duplicate members from group.
|
* Remove duplicate members from group.
|
||||||
|
|||||||
58
scm-webapp/src/main/java/sonia/scm/group/UserGroupCache.java
Normal file
58
scm-webapp/src/main/java/sonia/scm/group/UserGroupCache.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.group;
|
||||||
|
|
||||||
|
import sonia.scm.xml.XmlMapMultiStringAdapter;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptySet;
|
||||||
|
|
||||||
|
@XmlRootElement(name = "user-group-cache")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
class UserGroupCache {
|
||||||
|
@XmlJavaTypeAdapter(XmlMapMultiStringAdapter.class)
|
||||||
|
private Map<String, Set<String>> cache;
|
||||||
|
|
||||||
|
Set<String> get(String user) {
|
||||||
|
if (cache == null) {
|
||||||
|
return emptySet();
|
||||||
|
}
|
||||||
|
return cache.getOrDefault(user, emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
void put(String user, Set<String> groups) {
|
||||||
|
if (cache == null) {
|
||||||
|
cache = new HashMap<>();
|
||||||
|
}
|
||||||
|
cache.put(user, groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,8 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static sonia.scm.store.CopyOnWrite.compute;
|
||||||
|
|
||||||
abstract class DifferentiateBetweenConfigAndConfigEntryUpdateStep {
|
abstract class DifferentiateBetweenConfigAndConfigEntryUpdateStep {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DifferentiateBetweenConfigAndConfigEntryUpdateStep.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DifferentiateBetweenConfigAndConfigEntryUpdateStep.class);
|
||||||
@@ -90,7 +92,7 @@ abstract class DifferentiateBetweenConfigAndConfigEntryUpdateStep {
|
|||||||
private void updateSingleFile(Path configFile) {
|
private void updateSingleFile(Path configFile) {
|
||||||
LOG.info("Updating config entry file: {}", configFile);
|
LOG.info("Updating config entry file: {}", configFile);
|
||||||
|
|
||||||
Document configEntryDocument = readAsXmlDocument(configFile);
|
Document configEntryDocument = compute(() -> readAsXmlDocument(configFile)).withLockedFile(configFile);
|
||||||
|
|
||||||
configEntryDocument.getDocumentElement().setAttribute("type", "config-entry");
|
configEntryDocument.getDocumentElement().setAttribute("type", "config-entry");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.user;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static java.util.Collections.unmodifiableCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permission overview aggregates groups a user is a member of and all namespaces
|
||||||
|
* and repositories that have permissions configured for this user or one of its groups.
|
||||||
|
* This is the result of {@link PermissionOverviewCollector#create(String)}.
|
||||||
|
*
|
||||||
|
* @since 2.42.0
|
||||||
|
*/
|
||||||
|
public class PermissionOverview {
|
||||||
|
|
||||||
|
private final Collection<GroupEntry> relevantGroups;
|
||||||
|
private final Collection<String> relevantNamespaces;
|
||||||
|
private final Collection<Repository> relevantRepositories;
|
||||||
|
|
||||||
|
public PermissionOverview(Collection<GroupEntry> relevantGroups, Collection<String> relevantNamespaces, Collection<Repository> relevantRepositories) {
|
||||||
|
this.relevantGroups = relevantGroups;
|
||||||
|
this.relevantNamespaces = relevantNamespaces;
|
||||||
|
this.relevantRepositories = relevantRepositories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<GroupEntry> getRelevantGroups() {
|
||||||
|
return unmodifiableCollection(relevantGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<String> getRelevantNamespaces() {
|
||||||
|
return unmodifiableCollection(relevantNamespaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Repository> getRelevantRepositories() {
|
||||||
|
return unmodifiableCollection(relevantRepositories);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class GroupEntry {
|
||||||
|
private final String name;
|
||||||
|
private final boolean permissions;
|
||||||
|
private final boolean externalOnly;
|
||||||
|
|
||||||
|
public GroupEntry(String name, boolean permissions, boolean externalOnly) {
|
||||||
|
this.name = name;
|
||||||
|
this.permissions = permissions;
|
||||||
|
this.externalOnly = externalOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.user;
|
||||||
|
|
||||||
|
import sonia.scm.group.GroupCollector;
|
||||||
|
import sonia.scm.group.GroupManager;
|
||||||
|
import sonia.scm.repository.Namespace;
|
||||||
|
import sonia.scm.repository.NamespaceManager;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
|
import sonia.scm.repository.RepositoryPermission;
|
||||||
|
import sonia.scm.repository.RepositoryPermissionHolder;
|
||||||
|
import sonia.scm.security.PermissionAssigner;
|
||||||
|
import sonia.scm.security.PermissionDescriptor;
|
||||||
|
import sonia.scm.security.PermissionPermissions;
|
||||||
|
import sonia.scm.user.PermissionOverview.GroupEntry;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
|
public class PermissionOverviewCollector {
|
||||||
|
|
||||||
|
private final GroupCollector groupCollector;
|
||||||
|
private final PermissionAssigner permissionAssigner;
|
||||||
|
private final GroupManager groupManager;
|
||||||
|
private final RepositoryManager repositoryManager;
|
||||||
|
private final NamespaceManager namespaceManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PermissionOverviewCollector(GroupCollector groupCollector, PermissionAssigner permissionAssigner, GroupManager groupManager, RepositoryManager repositoryManager, NamespaceManager namespaceManager) {
|
||||||
|
this.groupCollector = groupCollector;
|
||||||
|
this.permissionAssigner = permissionAssigner;
|
||||||
|
this.groupManager = groupManager;
|
||||||
|
this.repositoryManager = repositoryManager;
|
||||||
|
this.namespaceManager = namespaceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionOverview create(String userId) {
|
||||||
|
PermissionPermissions.read().check();
|
||||||
|
Collection<String> groupsFromLastLogin = groupCollector.fromLastLoginPlusInternal(userId);
|
||||||
|
|
||||||
|
return new PermissionOverview(
|
||||||
|
collectGroups(groupsFromLastLogin),
|
||||||
|
collectNamespaces(userId, groupsFromLastLogin),
|
||||||
|
collectRepositories(userId, groupsFromLastLogin)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<GroupEntry> collectGroups(Collection<String> groupsFromLastLogin) {
|
||||||
|
Collection<GroupEntry> groupEntries = new ArrayList<>();
|
||||||
|
Collection<String> allGroups = groupManager.getAllNames();
|
||||||
|
groupsFromLastLogin.forEach(groupName -> {
|
||||||
|
Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForGroup(groupName);
|
||||||
|
groupEntries.add(
|
||||||
|
new GroupEntry(
|
||||||
|
groupName,
|
||||||
|
!permissionDescriptors.isEmpty(),
|
||||||
|
!allGroups.contains(groupName)));
|
||||||
|
});
|
||||||
|
return groupEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<String> collectNamespaces(String userId, Collection<String> groupsFromLastLogin) {
|
||||||
|
return namespaceManager
|
||||||
|
.getAll()
|
||||||
|
.stream()
|
||||||
|
.filter(namespace -> isRelevant(userId, groupsFromLastLogin, namespace))
|
||||||
|
.map(Namespace::getNamespace)
|
||||||
|
.collect(toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Repository> collectRepositories(String userId, Collection<String> groupsFromLastLogin) {
|
||||||
|
return repositoryManager
|
||||||
|
.getAll()
|
||||||
|
.stream()
|
||||||
|
.filter(repo -> isRelevant(userId, groupsFromLastLogin, repo))
|
||||||
|
.collect(toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRelevant(String userId, Collection<String> groupsFromLastLogin, RepositoryPermissionHolder permissionHolder) {
|
||||||
|
return permissionHolder.getPermissions().stream().anyMatch(permission -> isRelevant(userId, groupsFromLastLogin, permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRelevant(String userId, Collection<String> groupsFromLastLogin, RepositoryPermission permission) {
|
||||||
|
return permission.isGroupPermission() && groupsFromLastLogin.contains(permission.getName())
|
||||||
|
|| !permission.isGroupPermission() && userId.equals(permission.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.Links;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.group.Group;
|
||||||
|
import sonia.scm.group.GroupManager;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.user.PermissionOverview;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
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.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PermissionOverviewToPermissionOverviewDtoMapperTest {
|
||||||
|
|
||||||
|
public static final Repository REPOSITORY_1 = new Repository("1", "git", "hog", "marvin");
|
||||||
|
public static final Repository REPOSITORY_2 = new Repository("1", "git", "vogon", "jeltz");
|
||||||
|
|
||||||
|
public static final PermissionOverview PERMISSION_OVERVIEW = new PermissionOverview(
|
||||||
|
asList(
|
||||||
|
new PermissionOverview.GroupEntry("hitchhiker", true, false),
|
||||||
|
new PermissionOverview.GroupEntry("vogons", false, true)
|
||||||
|
),
|
||||||
|
asList("hog", "earth"),
|
||||||
|
asList(
|
||||||
|
REPOSITORY_1,
|
||||||
|
REPOSITORY_2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ResourceLinks resourceLinks;
|
||||||
|
@Mock
|
||||||
|
private NamespaceToNamespaceDtoMapper namespaceToNamespaceDtoMapper;
|
||||||
|
@Mock
|
||||||
|
private RepositoryToRepositoryDtoMapper repositoryToRepositoryDtoMapper;
|
||||||
|
@Mock
|
||||||
|
private GroupManager groupManager;
|
||||||
|
@Mock
|
||||||
|
private GroupToGroupDtoMapper groupToGroupDtoMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private PermissionOverviewToPermissionOverviewDtoMapperImpl permissionOverviewToPermissionOverviewDtoMapper;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initResourceLinks() {
|
||||||
|
when(resourceLinks.user())
|
||||||
|
.thenReturn(new ResourceLinks.UserLinks(() -> URI.create("/")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initRepositoryMapper() {
|
||||||
|
when(repositoryToRepositoryDtoMapper.map(any()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Repository repository = invocation.getArgument(0, Repository.class);
|
||||||
|
RepositoryDto repositoryDto = new RepositoryDto();
|
||||||
|
repositoryDto.setNamespace(repository.getNamespace());
|
||||||
|
repositoryDto.setName(repository.getName());
|
||||||
|
return repositoryDto;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initNamespaceMapper() {
|
||||||
|
when(namespaceToNamespaceDtoMapper.map(any()))
|
||||||
|
.thenAnswer(invocation -> new NamespaceDto(invocation.getArgument(0, String.class), Links.emptyLinks()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initGroupMapper() {
|
||||||
|
when(groupManager.get(anyString()))
|
||||||
|
.thenAnswer(invocation -> new Group("xml", invocation.getArgument(0, String.class)));
|
||||||
|
when(groupToGroupDtoMapper.map(any()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
GroupDto groupDto = new GroupDto();
|
||||||
|
groupDto.setName(invocation.getArgument(0, Group.class).getName());
|
||||||
|
return groupDto;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapRepositories() {
|
||||||
|
PermissionOverviewDto dto = permissionOverviewToPermissionOverviewDtoMapper
|
||||||
|
.toDto(PERMISSION_OVERVIEW, "Neo");
|
||||||
|
|
||||||
|
assertThat(dto.getRelevantRepositories())
|
||||||
|
.extracting("namespace")
|
||||||
|
.contains("hog", "vogon");
|
||||||
|
assertThat(dto.getRelevantRepositories())
|
||||||
|
.extracting("name")
|
||||||
|
.contains("marvin", "jeltz");
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
dto.
|
||||||
|
getEmbedded()
|
||||||
|
.getItemsBy("repositories")
|
||||||
|
).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapNamespaces() {
|
||||||
|
PermissionOverviewDto dto = permissionOverviewToPermissionOverviewDtoMapper
|
||||||
|
.toDto(PERMISSION_OVERVIEW, "Neo");
|
||||||
|
|
||||||
|
assertThat(dto.getRelevantNamespaces())
|
||||||
|
.contains("hog", "earth");
|
||||||
|
|
||||||
|
assertThat(dto.getEmbedded().getItemsBy("relevantNamespaces"))
|
||||||
|
.hasSize(2)
|
||||||
|
.extracting("namespace")
|
||||||
|
.contains("hog", "earth");
|
||||||
|
assertThat(dto.getEmbedded().getItemsBy("otherNamespaces"))
|
||||||
|
.hasSize(1)
|
||||||
|
.extracting("namespace")
|
||||||
|
.contains("vogon");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapGroups() {
|
||||||
|
PermissionOverviewDto dto = permissionOverviewToPermissionOverviewDtoMapper
|
||||||
|
.toDto(PERMISSION_OVERVIEW, "Neo");
|
||||||
|
|
||||||
|
assertThat(dto.getRelevantGroups())
|
||||||
|
.extracting("name")
|
||||||
|
.contains("hitchhiker", "vogons");
|
||||||
|
|
||||||
|
assertThat(dto.getEmbedded().getItemsBy("groups"))
|
||||||
|
.hasSize(2)
|
||||||
|
.extracting("name")
|
||||||
|
.contains("hitchhiker", "vogons");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,10 +41,13 @@ import org.mockito.Mock;
|
|||||||
import sonia.scm.ContextEntry;
|
import sonia.scm.ContextEntry;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.PageResult;
|
import sonia.scm.PageResult;
|
||||||
|
import sonia.scm.group.GroupManager;
|
||||||
import sonia.scm.security.ApiKeyService;
|
import sonia.scm.security.ApiKeyService;
|
||||||
import sonia.scm.security.PermissionAssigner;
|
import sonia.scm.security.PermissionAssigner;
|
||||||
import sonia.scm.security.PermissionDescriptor;
|
import sonia.scm.security.PermissionDescriptor;
|
||||||
import sonia.scm.user.ChangePasswordNotAllowedException;
|
import sonia.scm.user.ChangePasswordNotAllowedException;
|
||||||
|
import sonia.scm.user.PermissionOverview;
|
||||||
|
import sonia.scm.user.PermissionOverviewCollector;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
import sonia.scm.web.RestDispatcher;
|
import sonia.scm.web.RestDispatcher;
|
||||||
@@ -58,6 +61,8 @@ import java.net.URL;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import static de.otto.edison.hal.Links.emptyLinks;
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Collections.singletonList;
|
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.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -95,6 +100,16 @@ public class UserRootResourceTest {
|
|||||||
private ApiKeyService apiKeyService;
|
private ApiKeyService apiKeyService;
|
||||||
@Mock
|
@Mock
|
||||||
private PermissionAssigner permissionAssigner;
|
private PermissionAssigner permissionAssigner;
|
||||||
|
@Mock
|
||||||
|
private PermissionOverviewCollector permissionOverviewCollector;
|
||||||
|
@Mock
|
||||||
|
private RepositoryToRepositoryDtoMapper repositoryToRepositoryDtoMapper;
|
||||||
|
@Mock
|
||||||
|
private NamespaceToNamespaceDtoMapper namespaceToNamespaceDtoMapper;
|
||||||
|
@Mock
|
||||||
|
private GroupManager groupManager;
|
||||||
|
@Mock
|
||||||
|
private GroupToGroupDtoMapper groupToGroupDtoMapper;
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private UserDtoToUserMapperImpl dtoToUserMapper;
|
private UserDtoToUserMapperImpl dtoToUserMapper;
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
@@ -103,6 +118,8 @@ public class UserRootResourceTest {
|
|||||||
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
|
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private ApiKeyToApiKeyDtoMapperImpl apiKeyMapper;
|
private ApiKeyToApiKeyDtoMapperImpl apiKeyMapper;
|
||||||
|
@InjectMocks
|
||||||
|
private PermissionOverviewToPermissionOverviewDtoMapperImpl permissionOverviewMapper;
|
||||||
|
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<User> userCaptor;
|
private ArgumentCaptor<User> userCaptor;
|
||||||
@@ -110,6 +127,7 @@ public class UserRootResourceTest {
|
|||||||
private ArgumentCaptor<Predicate<User>> filterCaptor;
|
private ArgumentCaptor<Predicate<User>> filterCaptor;
|
||||||
|
|
||||||
private User originalUser;
|
private User originalUser;
|
||||||
|
private MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void prepareEnvironment() {
|
public void prepareEnvironment() {
|
||||||
@@ -125,7 +143,7 @@ public class UserRootResourceTest {
|
|||||||
UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper,
|
UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper,
|
||||||
userCollectionToDtoMapper, resourceLinks, passwordService);
|
userCollectionToDtoMapper, resourceLinks, passwordService);
|
||||||
UserPermissionResource userPermissionResource = new UserPermissionResource(permissionAssigner, permissionCollectionToDtoMapper);
|
UserPermissionResource userPermissionResource = new UserPermissionResource(permissionAssigner, permissionCollectionToDtoMapper);
|
||||||
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService, userPermissionResource);
|
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, permissionOverviewMapper, userManager, passwordService, userPermissionResource, permissionOverviewCollector);
|
||||||
ApiKeyCollectionToDtoMapper apiKeyCollectionToDtoMapper = new ApiKeyCollectionToDtoMapper(apiKeyMapper, resourceLinks);
|
ApiKeyCollectionToDtoMapper apiKeyCollectionToDtoMapper = new ApiKeyCollectionToDtoMapper(apiKeyMapper, resourceLinks);
|
||||||
UserApiKeyResource userApiKeyResource = new UserApiKeyResource(apiKeyService, apiKeyCollectionToDtoMapper, apiKeyMapper, resourceLinks);
|
UserApiKeyResource userApiKeyResource = new UserApiKeyResource(apiKeyService, apiKeyCollectionToDtoMapper, apiKeyMapper, resourceLinks);
|
||||||
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
|
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
|
||||||
@@ -137,7 +155,6 @@ public class UserRootResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldCreateFullResponseForAdmin() throws URISyntaxException, UnsupportedEncodingException {
|
public void shouldCreateFullResponseForAdmin() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -155,7 +172,6 @@ public class UserRootResourceTest {
|
|||||||
.post("/" + UserRootResource.USERS_PATH_V2)
|
.post("/" + UserRootResource.USERS_PATH_V2)
|
||||||
.contentType(VndMediaType.USER)
|
.contentType(VndMediaType.USER)
|
||||||
.content(userJson.getBytes());
|
.content(userJson.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -177,7 +193,6 @@ public class UserRootResourceTest {
|
|||||||
@SubjectAware(username = "unpriv")
|
@SubjectAware(username = "unpriv")
|
||||||
public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException, UnsupportedEncodingException {
|
public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -195,7 +210,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
@@ -213,7 +227,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
doThrow(new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("passwordChange", "-"), "xml")).when(userManager).overwritePassword(any(), any());
|
doThrow(new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("passwordChange", "-"), "xml")).when(userManager).overwritePassword(any(), any());
|
||||||
|
|
||||||
@@ -231,7 +244,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
doThrow(new NotFoundException("Test", "x")).when(userManager).overwritePassword(any(), any());
|
doThrow(new NotFoundException("Test", "x")).when(userManager).overwritePassword(any(), any());
|
||||||
|
|
||||||
@@ -249,7 +261,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
@@ -267,7 +278,6 @@ public class UserRootResourceTest {
|
|||||||
.post("/" + UserRootResource.USERS_PATH_V2)
|
.post("/" + UserRootResource.USERS_PATH_V2)
|
||||||
.contentType(VndMediaType.USER)
|
.contentType(VndMediaType.USER)
|
||||||
.content(userJson);
|
.content(userJson);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
|
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
@@ -287,7 +297,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo")
|
||||||
.contentType(VndMediaType.USER)
|
.contentType(VndMediaType.USER)
|
||||||
.content(userJson);
|
.content(userJson);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -303,7 +312,6 @@ public class UserRootResourceTest {
|
|||||||
.post("/" + UserRootResource.USERS_PATH_V2)
|
.post("/" + UserRootResource.USERS_PATH_V2)
|
||||||
.contentType(VndMediaType.USER)
|
.contentType(VndMediaType.USER)
|
||||||
.content(new byte[]{});
|
.content(new byte[]{});
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
|
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
@@ -314,7 +322,6 @@ public class UserRootResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldGetNotFoundForNotExistentUser() throws URISyntaxException {
|
public void shouldGetNotFoundForNotExistentUser() throws URISyntaxException {
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "nosuchuser");
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "nosuchuser");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -324,7 +331,6 @@ public class UserRootResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldDeleteUser() throws Exception {
|
public void shouldDeleteUser() throws Exception {
|
||||||
MockHttpRequest request = MockHttpRequest.delete("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
MockHttpRequest request = MockHttpRequest.delete("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -342,7 +348,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Other")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Other")
|
||||||
.contentType(VndMediaType.USER)
|
.contentType(VndMediaType.USER)
|
||||||
.content(userJson);
|
.content(userJson);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -360,7 +365,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo")
|
||||||
.contentType(VndMediaType.USER)
|
.contentType(VndMediaType.USER)
|
||||||
.content(userJson);
|
.content(userJson);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -373,7 +377,6 @@ public class UserRootResourceTest {
|
|||||||
PageResult<User> singletonPageResult = createSingletonPageResult(1);
|
PageResult<User> singletonPageResult = createSingletonPageResult(1);
|
||||||
when(userManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
|
when(userManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2);
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -389,7 +392,6 @@ public class UserRootResourceTest {
|
|||||||
PageResult<User> singletonPageResult = createSingletonPageResult(3);
|
PageResult<User> singletonPageResult = createSingletonPageResult(3);
|
||||||
when(userManager.getPage(any(), any(), eq(1), eq(1))).thenReturn(singletonPageResult);
|
when(userManager.getPage(any(), any(), eq(1), eq(1))).thenReturn(singletonPageResult);
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1");
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -407,7 +409,6 @@ public class UserRootResourceTest {
|
|||||||
PageResult<User> singletonPageResult = createSingletonPageResult(1);
|
PageResult<User> singletonPageResult = createSingletonPageResult(1);
|
||||||
when(userManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
|
when(userManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?q=One");
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?q=One");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -427,7 +428,6 @@ public class UserRootResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException {
|
public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -440,7 +440,6 @@ public class UserRootResourceTest {
|
|||||||
public void shouldGetPermissions() throws URISyntaxException, UnsupportedEncodingException {
|
public void shouldGetPermissions() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
when(permissionAssigner.readPermissionsForUser("Neo")).thenReturn(singletonList(new PermissionDescriptor("something:*")));
|
when(permissionAssigner.readPermissionsForUser("Neo")).thenReturn(singletonList(new PermissionDescriptor("something:*")));
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions");
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -455,7 +454,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions")
|
||||||
.contentType(VndMediaType.PERMISSION_COLLECTION)
|
.contentType(VndMediaType.PERMISSION_COLLECTION)
|
||||||
.content("{\"permissions\":[\"other:*\"]}".getBytes());
|
.content("{\"permissions\":[\"other:*\"]}".getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
ArgumentCaptor<Collection<PermissionDescriptor>> captor = ArgumentCaptor.forClass(Collection.class);
|
ArgumentCaptor<Collection<PermissionDescriptor>> captor = ArgumentCaptor.forClass(Collection.class);
|
||||||
doNothing().when(permissionAssigner).setPermissionsForUser(eq("Neo"), captor.capture());
|
doNothing().when(permissionAssigner).setPermissionsForUser(eq("Neo"), captor.capture());
|
||||||
|
|
||||||
@@ -466,6 +464,19 @@ public class UserRootResourceTest {
|
|||||||
assertEquals("other:*", captor.getValue().iterator().next().getValue());
|
assertEquals("other:*", captor.getValue().iterator().next().getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetPermissionsOverviewWithNamespaces() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
|
when(permissionOverviewCollector.create("Neo")).thenReturn(new PermissionOverview(emptyList(), singletonList("hog"), emptyList()));
|
||||||
|
when(namespaceToNamespaceDtoMapper.map("hog")).thenReturn(new NamespaceDto("hog", emptyLinks()));
|
||||||
|
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissionOverview");
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
|
assertThat(response.getContentAsString()).contains("hog");
|
||||||
|
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/users/Neo/permissionOverview\"}");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldConvertUserToInternalAndSetNewPassword() throws URISyntaxException {
|
public void shouldConvertUserToInternalAndSetNewPassword() throws URISyntaxException {
|
||||||
when(passwordService.encryptPassword(anyString())).thenReturn("abc");
|
when(passwordService.encryptPassword(anyString())).thenReturn("abc");
|
||||||
@@ -474,7 +485,6 @@ public class UserRootResourceTest {
|
|||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/convert-to-internal")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/convert-to-internal")
|
||||||
.contentType(VndMediaType.USER)
|
.contentType(VndMediaType.USER)
|
||||||
.content("{\"newPassword\":\"trillian\"}".getBytes());
|
.content("{\"newPassword\":\"trillian\"}".getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -492,7 +502,6 @@ public class UserRootResourceTest {
|
|||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/convert-to-external")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/convert-to-external")
|
||||||
.contentType(VndMediaType.USER);
|
.contentType(VndMediaType.USER);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import java.time.Instant;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.mockito.MockitoAnnotations.initMocks;
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
@@ -177,4 +176,16 @@ public class UserToUserDtoMapperTest {
|
|||||||
|
|
||||||
assertEquals("http://trillian", userDto.getLinks().getLinkBy("sample").get().getHref());
|
assertEquals("http://trillian", userDto.getLinks().getLinkBy("sample").get().getHref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldMapLinks_forPermissionOverview() {
|
||||||
|
User user = createDefaultUser();
|
||||||
|
when(subject.isPermitted("permission:read")).thenReturn(true);
|
||||||
|
when(subject.isPermitted("group:list")).thenReturn(true);
|
||||||
|
|
||||||
|
UserDto userDto = mapper.map(user);
|
||||||
|
|
||||||
|
assertEquals("expected permissions link", expectedBaseUri.resolve("abc/permissions").toString(), userDto.getLinks().getLinkBy("permissions").get().getHref());
|
||||||
|
assertEquals("expected permission overview link", expectedBaseUri.resolve("abc/permissionOverview").toString(), userDto.getLinks().getLinkBy("permissionOverview").get().getHref());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,21 @@ import org.junit.jupiter.api.BeforeEach;
|
|||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Answers;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.HandlerEventType;
|
import sonia.scm.HandlerEventType;
|
||||||
import sonia.scm.cache.MapCache;
|
import sonia.scm.cache.MapCache;
|
||||||
import sonia.scm.cache.MapCacheManager;
|
import sonia.scm.cache.MapCacheManager;
|
||||||
import sonia.scm.security.LogoutEvent;
|
import sonia.scm.security.LogoutEvent;
|
||||||
|
import sonia.scm.store.ConfigurationStore;
|
||||||
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserEvent;
|
import sonia.scm.user.UserEvent;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -59,6 +63,11 @@ class DefaultGroupCollectorTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private GroupResolver groupResolver;
|
private GroupResolver groupResolver;
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private ConfigurationStoreFactory configurationStoreFactory;
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private ConfigurationStore configurationStore;
|
||||||
|
|
||||||
private MapCacheManager mapCacheManager;
|
private MapCacheManager mapCacheManager;
|
||||||
|
|
||||||
private Set<GroupResolver> groupResolvers;
|
private Set<GroupResolver> groupResolvers;
|
||||||
@@ -69,7 +78,14 @@ class DefaultGroupCollectorTest {
|
|||||||
void initCollector() {
|
void initCollector() {
|
||||||
groupResolvers = new HashSet<>();
|
groupResolvers = new HashSet<>();
|
||||||
mapCacheManager = new MapCacheManager();
|
mapCacheManager = new MapCacheManager();
|
||||||
collector = new DefaultGroupCollector(groupDAO, mapCacheManager, groupResolvers);
|
collector = new DefaultGroupCollector(groupDAO, mapCacheManager, groupResolvers, configurationStoreFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initStore() {
|
||||||
|
when(configurationStoreFactory.withType(UserGroupCache.class).withName("user-group-cache").build())
|
||||||
|
.thenReturn(configurationStore);
|
||||||
|
when(configurationStore.getOptional()).thenReturn(Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -141,7 +157,16 @@ class DefaultGroupCollectorTest {
|
|||||||
verify(groupDAO, never()).getAll();
|
verify(groupDAO, never()).getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetCachedGroupsFromLastLogin() {
|
||||||
|
UserGroupCache cache = new UserGroupCache();
|
||||||
|
cache.put("trillian", Set.of("hog"));
|
||||||
|
when(configurationStore.getOptional()).thenReturn(Optional.of(cache));
|
||||||
|
|
||||||
|
Set<String> cachedGroups = collector.fromLastLoginPlusInternal("trillian");
|
||||||
|
|
||||||
|
assertThat(cachedGroups).contains("hog");
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class WithGroupsFromDao {
|
class WithGroupsFromDao {
|
||||||
@@ -169,5 +194,23 @@ class DefaultGroupCollectorTest {
|
|||||||
Iterable<String> groupNames = collector.collect("trillian");
|
Iterable<String> groupNames = collector.collect("trillian");
|
||||||
assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible");
|
assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetScmGroupsForLastLoginWhenNothingCached() {
|
||||||
|
Set<String> cachedGroups = collector.fromLastLoginPlusInternal("trillian");
|
||||||
|
|
||||||
|
assertThat(cachedGroups).contains("heartOfGold", "fjordsOfAfrican");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetCachedGroupsFromLastLoginWithInternalGroups() {
|
||||||
|
UserGroupCache cache = new UserGroupCache();
|
||||||
|
cache.put("trillian", Set.of("earth"));
|
||||||
|
when(configurationStore.getOptional()).thenReturn(Optional.of(cache));
|
||||||
|
|
||||||
|
Set<String> cachedGroups = collector.fromLastLoginPlusInternal("trillian");
|
||||||
|
|
||||||
|
assertThat(cachedGroups).contains("earth", "heartOfGold", "fjordsOfAfrican");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.user;
|
||||||
|
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.group.Group;
|
||||||
|
import sonia.scm.group.GroupCollector;
|
||||||
|
import sonia.scm.group.GroupManager;
|
||||||
|
import sonia.scm.repository.Namespace;
|
||||||
|
import sonia.scm.repository.NamespaceManager;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
|
import sonia.scm.repository.RepositoryPermission;
|
||||||
|
import sonia.scm.repository.RepositoryTestData;
|
||||||
|
import sonia.scm.security.PermissionAssigner;
|
||||||
|
import sonia.scm.security.PermissionDescriptor;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static java.util.Collections.singleton;
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PermissionOverviewCollectorTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GroupCollector groupCollector;
|
||||||
|
@Mock
|
||||||
|
private PermissionAssigner permissionAssigner;
|
||||||
|
@Mock
|
||||||
|
private GroupManager groupManager;
|
||||||
|
@Mock
|
||||||
|
private RepositoryManager repositoryManager;
|
||||||
|
@Mock
|
||||||
|
private NamespaceManager namespaceManager;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private PermissionOverviewCollector permissionOverviewCollector;
|
||||||
|
|
||||||
|
private final String unknownGroupName = "hog";
|
||||||
|
private final String knownGroupName = "earth";
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockGroups() {
|
||||||
|
when(groupCollector.fromLastLoginPlusInternal("trillian"))
|
||||||
|
.thenReturn(Set.of(unknownGroupName, knownGroupName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockSubject() {
|
||||||
|
Subject subject = mock(Subject.class);
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void clearContext() {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithGroups {
|
||||||
|
@Test
|
||||||
|
void shouldCollectGroupsFromGroupCollector() {
|
||||||
|
when(groupManager.getAllNames()).thenReturn(singleton(knownGroupName));
|
||||||
|
mockUnknownGroup(unknownGroupName);
|
||||||
|
mockKnownGroup(knownGroupName);
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
Collection<PermissionOverview.GroupEntry> relevantGroups = permissionOverview.getRelevantGroups();
|
||||||
|
assertThat(relevantGroups)
|
||||||
|
.extracting("name")
|
||||||
|
.contains(unknownGroupName, knownGroupName);
|
||||||
|
assertThat(relevantGroups)
|
||||||
|
.extracting("permissions")
|
||||||
|
.contains(false, true);
|
||||||
|
assertThat(relevantGroups)
|
||||||
|
.extracting("externalOnly")
|
||||||
|
.contains(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockKnownGroup(String knownGroupName) {
|
||||||
|
when(permissionAssigner.readPermissionsForGroup(knownGroupName))
|
||||||
|
.thenReturn(singleton(new PermissionDescriptor()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockUnknownGroup(String unknownGroupName) {
|
||||||
|
when(permissionAssigner.readPermissionsForGroup(unknownGroupName))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithNamespaces {
|
||||||
|
|
||||||
|
private Namespace namespace = new Namespace("git");
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockNamespace() {
|
||||||
|
when(namespaceManager.getAll())
|
||||||
|
.thenReturn(singletonList(namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindNamespaces() {
|
||||||
|
namespace.addPermission(new RepositoryPermission("trillian", "read", false));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantNamespaces())
|
||||||
|
.contains("git");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindNamespacesWithPermissionForUser() {
|
||||||
|
namespace.addPermission(new RepositoryPermission("trillian", "read", false));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantNamespaces())
|
||||||
|
.contains("git");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindNamespaceWithPermissionForGroupOfUser() {
|
||||||
|
namespace.addPermission(new RepositoryPermission(knownGroupName, "read", true));
|
||||||
|
when(groupCollector.fromLastLoginPlusInternal("trillian"))
|
||||||
|
.thenReturn(singleton(knownGroupName));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantNamespaces())
|
||||||
|
.contains("git");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIgnoreNamespaceWithPermissionForOtherUser() {
|
||||||
|
namespace.addPermission(new RepositoryPermission("arthur", "read", false));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantNamespaces())
|
||||||
|
.doesNotContain("git");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIgnoreRepositoryWithPermissionForOtherGroups() {
|
||||||
|
namespace.addPermission(new RepositoryPermission("vogons", "read", true));
|
||||||
|
when(groupCollector.fromLastLoginPlusInternal("trillian"))
|
||||||
|
.thenReturn(singleton(knownGroupName));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantNamespaces())
|
||||||
|
.doesNotContain("git");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithRepositories {
|
||||||
|
|
||||||
|
private final Repository repository = RepositoryTestData.create42Puzzle();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockRepository() {
|
||||||
|
when(repositoryManager.getAll())
|
||||||
|
.thenReturn(singletonList(repository));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindRepositoryWithPermissionForUser() {
|
||||||
|
repository.addPermission(new RepositoryPermission("trillian", "read", false));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantRepositories())
|
||||||
|
.contains(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindRepositoryWithPermissionForGroupOfUser() {
|
||||||
|
repository.addPermission(new RepositoryPermission(knownGroupName, "read", true));
|
||||||
|
when(groupCollector.fromLastLoginPlusInternal("trillian"))
|
||||||
|
.thenReturn(singleton(knownGroupName));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantRepositories())
|
||||||
|
.contains(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIgnoreRepositoryWithPermissionForOtherUser() {
|
||||||
|
repository.addPermission(new RepositoryPermission("arthur", "read", false));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantRepositories())
|
||||||
|
.doesNotContain(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIgnoreRepositoryWithPermissionForOtherGroups() {
|
||||||
|
repository.addPermission(new RepositoryPermission("vogons", "read", true));
|
||||||
|
when(groupCollector.fromLastLoginPlusInternal("trillian"))
|
||||||
|
.thenReturn(singleton(knownGroupName));
|
||||||
|
|
||||||
|
PermissionOverview permissionOverview = permissionOverviewCollector.create("trillian");
|
||||||
|
|
||||||
|
assertThat(permissionOverview.getRelevantRepositories())
|
||||||
|
.doesNotContain(repository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user