mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 09:46:16 +01:00
Fix audit log issues:
- Use store name as label for repository related changes if no explicit labels are set. - Introduce 'ignore' flag - Fix missing call to store - Create audit logs for permissions - Set flex attributes for input field to use full available space Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com> Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
committed by
SCM-Manager
parent
1d0baf48e2
commit
b511789620
@@ -25,14 +25,17 @@
|
||||
package sonia.scm.auditlog;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.LOCAL_VARIABLE, ElementType.FIELD, ElementType.PACKAGE, ElementType.METHOD})
|
||||
@Target({ElementType.TYPE})
|
||||
public @interface AuditEntry {
|
||||
String[] labels() default {};
|
||||
String[] maskedFields() default {};
|
||||
String[] ignoredFields() default {};
|
||||
boolean ignore() default false;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,11 @@ import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.StoreDecoratorFactory;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
public class AuditLogConfigurationStoreDecorator<T> implements ConfigurationStore<T> {
|
||||
|
||||
private final Set<Auditor> auditors;
|
||||
@@ -50,13 +53,40 @@ public class AuditLogConfigurationStoreDecorator<T> implements ConfigurationStor
|
||||
}
|
||||
|
||||
public void set(T object) {
|
||||
String repositoryId = context.getStoreParameters().getRepositoryId();
|
||||
if (!Strings.isNullOrEmpty(repositoryId)) {
|
||||
String name = repositoryDAO.get(repositoryId).getNamespaceAndName().toString();
|
||||
auditors.forEach(s -> s.createEntry(new EntryCreationContext<>(object, get(), name, Set.of("repository"))));
|
||||
} else {
|
||||
auditors.forEach(s -> s.createEntry(new EntryCreationContext<>(object, get(), "", Set.of(context.getStoreParameters().getName()))));
|
||||
}
|
||||
delegate.set(object);
|
||||
if (!shouldBeIgnored(object)) {
|
||||
auditors.forEach(s -> s.createEntry(createEntryCreationContext(object)));
|
||||
}
|
||||
delegate.set(object);
|
||||
}
|
||||
|
||||
private EntryCreationContext<T> createEntryCreationContext(T object) {
|
||||
String repositoryId = context.getStoreParameters().getRepositoryId();
|
||||
if (!Strings.isNullOrEmpty(repositoryId)) {
|
||||
String name = repositoryDAO.get(repositoryId).getNamespaceAndName().toString();
|
||||
return new EntryCreationContext<>(object, get(), name, getRepositoryLabels(object));
|
||||
} else {
|
||||
return new EntryCreationContext<>(object, get(), "", shouldUseStoreNameAsLabel(object) ? Set.of(context.getStoreParameters().getName()) : emptySet());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldBeIgnored(T object) {
|
||||
return getAnnotation(object).map(AuditEntry::ignore).orElse(false);
|
||||
}
|
||||
|
||||
private Set<String> getRepositoryLabels(T object) {
|
||||
Set<String> labels = new java.util.HashSet<>();
|
||||
labels.add("repository");
|
||||
if (shouldUseStoreNameAsLabel(object)) {
|
||||
labels.add(context.getStoreParameters().getName());
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
private boolean shouldUseStoreNameAsLabel(T object) {
|
||||
return getAnnotation(object).map(annotation -> annotation.labels().length == 0).orElse(true);
|
||||
}
|
||||
|
||||
private Optional<AuditEntry> getAnnotation(T object) {
|
||||
return Optional.ofNullable(object.getClass().getAnnotation(AuditEntry.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ package sonia.scm.security;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import sonia.scm.auditlog.AuditEntry;
|
||||
import sonia.scm.auditlog.AuditLogEntity;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
@@ -47,7 +48,7 @@ import java.io.Serializable;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "assigned-permission")
|
||||
@AuditEntry(labels = "permission")
|
||||
public class AssignedPermission implements PermissionObject, Serializable
|
||||
public class AssignedPermission implements PermissionObject, Serializable, AuditLogEntity
|
||||
{
|
||||
|
||||
/** serial version uid */
|
||||
@@ -207,4 +208,9 @@ public class AssignedPermission implements PermissionObject, Serializable
|
||||
|
||||
/** string representation of the permission */
|
||||
private PermissionDescriptor permission;
|
||||
|
||||
@Override
|
||||
public String getEntityName() {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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.auditlog;
|
||||
|
||||
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.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.StoreDecoratorFactory;
|
||||
import sonia.scm.store.TypedStoreParameters;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuditLogConfigurationStoreDecoratorTest {
|
||||
|
||||
@Mock
|
||||
private Auditor auditor;
|
||||
@Mock
|
||||
private RepositoryDAO repositoryDAO;
|
||||
@Mock
|
||||
private ConfigurationStore<Object> delegate;
|
||||
@Mock
|
||||
private StoreDecoratorFactory.Context storeContext;
|
||||
@Mock
|
||||
private TypedStoreParameters parameters;
|
||||
|
||||
private AuditLogConfigurationStoreDecorator<Object> decorator;
|
||||
|
||||
@BeforeEach
|
||||
void setUpDecorator() {
|
||||
decorator = new AuditLogConfigurationStoreDecorator<>(Set.of(auditor), repositoryDAO, delegate, storeContext);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithAuditableEntries {
|
||||
|
||||
@BeforeEach
|
||||
void setUpStoreContext() {
|
||||
when(storeContext.getStoreParameters()).thenReturn(parameters);
|
||||
lenient().when(parameters.getName()).thenReturn("hog");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallAuditorForSimpleEntry() {
|
||||
Object entry = new SimpleEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEmpty();
|
||||
assertThat(context.getAdditionalLabels()).contains("hog");
|
||||
assertThat(context.getObject()).isSameAs(entry);
|
||||
assertThat(context.getOldObject()).isNull();
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallAuditorForAdditionalLabelEntry() {
|
||||
Object entry = new ExtraLabelEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEmpty();
|
||||
assertThat(context.getAdditionalLabels()).isEmpty();
|
||||
assertThat(context.getObject()).isSameAs(entry);
|
||||
assertThat(context.getOldObject()).isNull();
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallDelegateForSimpleEntry() {
|
||||
Object entry = new SimpleEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(delegate).set(entry);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ForRepositoryStore {
|
||||
|
||||
@BeforeEach
|
||||
void mockRepositoryContext() {
|
||||
when(parameters.getRepositoryId()).thenReturn("42");
|
||||
when(repositoryDAO.get("42")).thenReturn(new Repository("42", "git", "hitchhiker", "hog"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallAuditorForSimpleEntry() {
|
||||
Object entry = new SimpleEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("hitchhiker/hog");
|
||||
assertThat(context.getAdditionalLabels()).contains("hog");
|
||||
assertThat(context.getObject()).isSameAs(entry);
|
||||
assertThat(context.getOldObject()).isNull();
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallAuditorForAdditionalLabelEntry() {
|
||||
Object entry = new ExtraLabelEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("hitchhiker/hog");
|
||||
assertThat(context.getAdditionalLabels()).contains("repository");
|
||||
assertThat(context.getObject()).isSameAs(entry);
|
||||
assertThat(context.getOldObject()).isNull();
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseOldObjectFromStore() {
|
||||
Object oldObject = new Object();
|
||||
when(delegate.get()).thenReturn(oldObject);
|
||||
|
||||
Object entry = new SimpleEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getOldObject()).isSameAs(oldObject);
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseOldObjectFromStore() {
|
||||
Object oldObject = new Object();
|
||||
when(delegate.get()).thenReturn(oldObject);
|
||||
|
||||
Object entry = new SimpleEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getOldObject()).isSameAs(oldObject);
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCallAuditorForIgnoredEntry() {
|
||||
Object entry = new IgnoredEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(auditor, never()).createEntry(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallDelegateForIgnoredEntry() {
|
||||
Object entry = new IgnoredEntry();
|
||||
|
||||
decorator.set(entry);
|
||||
|
||||
verify(delegate).set(entry);
|
||||
}
|
||||
|
||||
@AuditEntry
|
||||
private static class SimpleEntry {
|
||||
}
|
||||
|
||||
@AuditEntry(labels = "permission")
|
||||
private static class ExtraLabelEntry {
|
||||
}
|
||||
|
||||
@AuditEntry(ignore = true)
|
||||
private static class IgnoredEntry {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.group.xml;
|
||||
|
||||
import sonia.scm.auditlog.AuditEntry;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.xml.XmlDatabase;
|
||||
|
||||
@@ -40,6 +41,7 @@ import java.util.TreeMap;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@AuditEntry(ignore = true)
|
||||
@XmlRootElement(name = "group-db")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class XmlGroupDatabase implements XmlDatabase<Group>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.repository.xml;
|
||||
|
||||
import sonia.scm.auditlog.AuditEntry;
|
||||
import sonia.scm.repository.RepositoryRole;
|
||||
import sonia.scm.xml.XmlDatabase;
|
||||
|
||||
@@ -36,6 +37,7 @@ import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@AuditEntry(ignore = true)
|
||||
@XmlRootElement(name = "user-db")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class XmlRepositoryRoleDatabase implements XmlDatabase<RepositoryRole> {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.user.xml;
|
||||
|
||||
import sonia.scm.auditlog.AuditEntry;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.xml.XmlDatabase;
|
||||
|
||||
@@ -40,6 +41,7 @@ import java.util.TreeMap;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@AuditEntry(ignore = true)
|
||||
@XmlRootElement(name = "user-db")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class XmlUserDatabase implements XmlDatabase<User>
|
||||
|
||||
@@ -36,7 +36,7 @@ type Props = {
|
||||
const Select = React.forwardRef<HTMLSelectElement, Props>(
|
||||
({ variant, children, className, options, testId, ...props }, ref) => (
|
||||
<div className={classNames("select", { "is-multiple": props.multiple }, createVariantClass(variant), className)}>
|
||||
<select ref={ref} {...props} {...createAttributesForTesting(testId)}>
|
||||
<select ref={ref} {...props} {...createAttributesForTesting(testId)} className={className}>
|
||||
{options
|
||||
? options.map((option) => (
|
||||
<option {...option} key={option.value as Key}>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.group;
|
||||
|
||||
import sonia.scm.auditlog.AuditEntry;
|
||||
import sonia.scm.xml.XmlMapMultiStringAdapter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
@@ -36,6 +37,7 @@ import java.util.Set;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
@AuditEntry(ignore = true)
|
||||
@XmlRootElement(name = "user-group-cache")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
class UserGroupCache {
|
||||
|
||||
@@ -36,6 +36,9 @@ import com.google.inject.Singleton;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.auditlog.AuditEntry;
|
||||
import sonia.scm.auditlog.Auditor;
|
||||
import sonia.scm.auditlog.EntryCreationContext;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
@@ -52,19 +55,18 @@ import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
|
||||
/**
|
||||
* TODO add events
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.31
|
||||
*/
|
||||
@@ -76,19 +78,23 @@ public class DefaultSecuritySystem implements SecuritySystem {
|
||||
private static final String PERMISSION_DESCRIPTOR =
|
||||
"META-INF/scm/permissions.xml";
|
||||
|
||||
/**
|
||||
* the logger for DefaultSecuritySystem
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(DefaultSecuritySystem.class);
|
||||
|
||||
private final ConfigurationEntryStore<AssignedPermission> store;
|
||||
|
||||
private final ImmutableSet<PermissionDescriptor> availablePermissions;
|
||||
|
||||
private final Set<Auditor> auditors;
|
||||
|
||||
@Inject
|
||||
public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory, PluginLoader pluginLoader) {
|
||||
public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory, PluginLoader pluginLoader, Set<Auditor> auditors) {
|
||||
store = storeFactory
|
||||
.withType(AssignedPermission.class)
|
||||
.withName(NAME)
|
||||
.build();
|
||||
this.availablePermissions = readAvailablePermissions(pluginLoader);
|
||||
this.auditors = auditors;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,9 +102,9 @@ public class DefaultSecuritySystem implements SecuritySystem {
|
||||
assertHasPermission();
|
||||
validatePermission(permission);
|
||||
|
||||
String id = store.put(permission);
|
||||
callAuditors(null, permission);
|
||||
|
||||
StoredAssignedPermission sap = new StoredAssignedPermission(id, permission);
|
||||
store.put(permission);
|
||||
|
||||
//J-
|
||||
ScmEventBus.getInstance().post(
|
||||
@@ -114,6 +120,7 @@ public class DefaultSecuritySystem implements SecuritySystem {
|
||||
&& Objects.equal(sap.isGroupPermission(), permission.isGroupPermission())
|
||||
&& Objects.equal(sap.getPermission(), permission.getPermission()));
|
||||
if (deleted) {
|
||||
callAuditors(permission, null);
|
||||
ScmEventBus.getInstance().post(
|
||||
new AssignedPermissionEvent(HandlerEventType.DELETE, permission)
|
||||
);
|
||||
@@ -170,10 +177,9 @@ public class DefaultSecuritySystem implements SecuritySystem {
|
||||
return !toRemove.isEmpty();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<PermissionDescriptor> parsePermissionDescriptor(
|
||||
JAXBContext context, URL descriptorUrl) {
|
||||
List<PermissionDescriptor> descriptors = Collections.EMPTY_LIST;
|
||||
List<PermissionDescriptor> descriptors = emptyList();
|
||||
|
||||
try {
|
||||
PermissionDescriptors descriptorWrapper =
|
||||
@@ -227,6 +233,15 @@ public class DefaultSecuritySystem implements SecuritySystem {
|
||||
"permission is required");
|
||||
}
|
||||
|
||||
private void callAuditors(AssignedPermission notModified, AssignedPermission newObject) {
|
||||
AssignedPermission nonNullPermission = newObject == null ? notModified : newObject;
|
||||
if (nonNullPermission.getClass().isAnnotationPresent(AuditEntry.class)) {
|
||||
String label = nonNullPermission.isGroupPermission() ? "group" : "user";
|
||||
EntryCreationContext<AssignedPermission> context = new EntryCreationContext<>(newObject, notModified, nonNullPermission.getEntityName(), Set.of(label));
|
||||
auditors.forEach(s -> s.createEntry(context));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor for permissions.
|
||||
*/
|
||||
@@ -234,10 +249,9 @@ public class DefaultSecuritySystem implements SecuritySystem {
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class PermissionDescriptors {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<PermissionDescriptor> getPermissions() {
|
||||
if (permissions == null) {
|
||||
permissions = Collections.EMPTY_LIST;
|
||||
permissions = emptyList();
|
||||
}
|
||||
|
||||
return permissions;
|
||||
@@ -246,8 +260,4 @@ public class DefaultSecuritySystem implements SecuritySystem {
|
||||
@XmlElement(name = "permission")
|
||||
private List<PermissionDescriptor> permissions;
|
||||
}
|
||||
|
||||
private final ConfigurationEntryStore<AssignedPermission> store;
|
||||
|
||||
private final ImmutableSet<PermissionDescriptor> availablePermissions;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public class PermissionAssigner {
|
||||
permissions.stream()
|
||||
.filter(permissionExists(availablePermissions, existingPermissions))
|
||||
.map(p -> new AssignedPermission(id, groupPermission, p))
|
||||
.filter(p -> !existingPermissions.contains(p))
|
||||
.filter(p -> existingPermissions.stream().map(AssignedPermission::getPermission).noneMatch(p2 -> p.getPermission().equals(p2)))
|
||||
.forEach(securitySystem::addPermission);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,21 +30,26 @@ import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||
import org.apache.shiro.realm.SimpleAccountRealm;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import sonia.scm.AbstractTestBase;
|
||||
import sonia.scm.auditlog.Auditor;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||
import sonia.scm.util.ClassLoaders;
|
||||
import sonia.scm.util.MockUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -56,14 +61,11 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
|
||||
private JAXBConfigurationEntryStoreFactory jaxbConfigurationEntryStoreFactory;
|
||||
private PluginLoader pluginLoader;
|
||||
@InjectMocks
|
||||
@Mock
|
||||
private Auditor auditor;
|
||||
|
||||
private DefaultSecuritySystem securitySystem;
|
||||
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Before
|
||||
public void createSecuritySystem()
|
||||
{
|
||||
@@ -73,12 +75,9 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
securitySystem = new DefaultSecuritySystem(jaxbConfigurationEntryStoreFactory, pluginLoader, Set.of(auditor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testAddPermission()
|
||||
{
|
||||
@@ -91,10 +90,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
assertEquals(false, sap.isGroupPermission());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testAvailablePermissions()
|
||||
{
|
||||
@@ -106,10 +101,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
assertThat(list).isNotEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testDeletePermission()
|
||||
{
|
||||
@@ -123,10 +114,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
assertThat(securitySystem.getPermissions(p -> p.getName().equals("trillian"))).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testGetAllPermissions()
|
||||
{
|
||||
@@ -145,10 +132,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
assertThat(all).contains(trillian, dent, marvin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testGetPermission()
|
||||
{
|
||||
@@ -162,10 +145,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
assertThat(other).containsExactly(sap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testGetPermissionsWithPredicate()
|
||||
{
|
||||
@@ -186,10 +165,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
.contains(trillian, dent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = UnauthorizedException.class)
|
||||
public void testUnauthorizedAddPermission()
|
||||
{
|
||||
@@ -197,10 +172,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
createPermission("trillian", false, "repository:*:READ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = UnauthorizedException.class)
|
||||
public void testUnauthorizedDeletePermission()
|
||||
{
|
||||
@@ -213,16 +184,94 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
securitySystem.deletePermission(sap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param name
|
||||
* @param groupPermission
|
||||
* @param value
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Test
|
||||
public void shouldCallAuditorForNewUserPermission()
|
||||
{
|
||||
setAdminSubject();
|
||||
|
||||
createPermission("trillian", false, "repository:*:READ");
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("trillian");
|
||||
assertThat(context.getAdditionalLabels()).contains("user");
|
||||
assertThat(context.getOldObject()).isNull();
|
||||
assertThat(context.getObject())
|
||||
.extracting("permission")
|
||||
.extracting("value")
|
||||
.isEqualTo("repository:*:READ");
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCallAuditorForNewGroupPermission()
|
||||
{
|
||||
setAdminSubject();
|
||||
|
||||
createPermission("trillian", true, "repository:*:READ");
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("trillian");
|
||||
assertThat(context.getAdditionalLabels()).contains("group");
|
||||
assertThat(context.getOldObject()).isNull();
|
||||
assertThat(context.getObject())
|
||||
.extracting("permission")
|
||||
.extracting("value")
|
||||
.isEqualTo("repository:*:READ");
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCallAuditorForRemovedUserPermission()
|
||||
{
|
||||
setAdminSubject();
|
||||
|
||||
createPermission("trillian", false, "repository:*:READ");
|
||||
reset(auditor);
|
||||
securitySystem.deletePermission(new AssignedPermission("trillian", false, "repository:*:READ"));
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("trillian");
|
||||
assertThat(context.getAdditionalLabels()).contains("user");
|
||||
assertThat(context.getObject()).isNull();
|
||||
assertThat(context.getOldObject())
|
||||
.extracting("permission")
|
||||
.extracting("value")
|
||||
.isEqualTo("repository:*:READ");
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCallAuditorForRemovedGroupPermission()
|
||||
{
|
||||
setAdminSubject();
|
||||
|
||||
createPermission("trillian", true, "repository:*:READ");
|
||||
reset(auditor);
|
||||
securitySystem.deletePermission(new AssignedPermission("trillian", true, "repository:*:READ"));
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("trillian");
|
||||
assertThat(context.getAdditionalLabels()).contains("group");
|
||||
assertThat(context.getObject()).isNull();
|
||||
assertThat(context.getOldObject())
|
||||
.extracting("permission")
|
||||
.extracting("value")
|
||||
.isEqualTo("repository:*:READ");
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private AssignedPermission createPermission(String name,
|
||||
boolean groupPermission, String value)
|
||||
{
|
||||
@@ -235,21 +284,11 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
&& Objects.equal(value, permission.getPermission().getValue())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found"));
|
||||
}
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
private void setAdminSubject()
|
||||
{
|
||||
setSubject(MockUtil.createAdminSubject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
private void setUserSubject()
|
||||
{
|
||||
org.apache.shiro.mgt.SecurityManager sm =
|
||||
|
||||
@@ -27,22 +27,28 @@ package sonia.scm.security;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.auditlog.Auditor;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
|
||||
import sonia.scm.util.ClassLoaders;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini", username = "dent", password = "secret")
|
||||
@@ -57,12 +63,14 @@ public class PermissionAssignerTest {
|
||||
private DefaultSecuritySystem securitySystem;
|
||||
private PermissionAssigner permissionAssigner;
|
||||
|
||||
private Auditor auditor = mock(Auditor.class);
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
PluginLoader pluginLoader = mock(PluginLoader.class);
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
|
||||
|
||||
securitySystem = new DefaultSecuritySystem(new InMemoryConfigurationEntryStoreFactory(), pluginLoader) {
|
||||
securitySystem = new DefaultSecuritySystem(new InMemoryConfigurationEntryStoreFactory(), pluginLoader, Set.of(auditor)) {
|
||||
@Override
|
||||
public Collection<PermissionDescriptor> getAvailablePermissions() {
|
||||
return Arrays.stream(new String[]{"perm:read:1", "perm:read:2", "perm:read:3", "perm:read:4"})
|
||||
@@ -86,14 +94,14 @@ public class PermissionAssignerTest {
|
||||
public void shouldFindUserPermissions() {
|
||||
Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("1");
|
||||
|
||||
Assertions.assertThat(permissionDescriptors).hasSize(2);
|
||||
assertThat(permissionDescriptors).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFindGroupPermissions() {
|
||||
Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("1");
|
||||
|
||||
Assertions.assertThat(permissionDescriptors).hasSize(2);
|
||||
assertThat(permissionDescriptors).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -110,7 +118,7 @@ public class PermissionAssignerTest {
|
||||
|
||||
Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("2");
|
||||
|
||||
Assertions.assertThat(permissionDescriptors).hasSize(2);
|
||||
assertThat(permissionDescriptors).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -132,5 +140,47 @@ public class PermissionAssignerTest {
|
||||
securitySystem.addPermission(new AssignedPermission("2", "perm:read:5"));
|
||||
|
||||
permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:5")));
|
||||
|
||||
assertThat(permissionAssigner.readPermissionsForUser("2")).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCallAuditorForCreation() {
|
||||
reset(auditor);
|
||||
|
||||
permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:2"), new PermissionDescriptor("perm:read:4")));
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("2");
|
||||
assertThat(context.getAdditionalLabels()).contains("user");
|
||||
assertThat(context.getOldObject()).isNull();
|
||||
assertThat(context.getObject())
|
||||
.extracting("permission")
|
||||
.extracting("value")
|
||||
.isEqualTo("perm:read:4");
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCallAuditorForRemoval() {
|
||||
reset(auditor);
|
||||
|
||||
permissionAssigner.setPermissionsForUser("2", emptyList());
|
||||
|
||||
verify(auditor).createEntry(argThat(
|
||||
context -> {
|
||||
assertThat(context.getEntity()).isEqualTo("2");
|
||||
assertThat(context.getAdditionalLabels()).contains("user");
|
||||
assertThat(context.getObject()).isNull();
|
||||
assertThat(context.getOldObject())
|
||||
.extracting("permission")
|
||||
.extracting("value")
|
||||
.isEqualTo("perm:read:2");
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user