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:
Eduard Heimbuch
2023-03-21 12:03:51 +01:00
committed by SCM-Manager
parent 1d0baf48e2
commit b511789620
13 changed files with 473 additions and 96 deletions

View File

@@ -25,14 +25,17 @@
package sonia.scm.auditlog; package sonia.scm.auditlog;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@Inherited
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.LOCAL_VARIABLE, ElementType.FIELD, ElementType.PACKAGE, ElementType.METHOD}) @Target({ElementType.TYPE})
public @interface AuditEntry { public @interface AuditEntry {
String[] labels() default {}; String[] labels() default {};
String[] maskedFields() default {}; String[] maskedFields() default {};
String[] ignoredFields() default {}; String[] ignoredFields() default {};
boolean ignore() default false;
} }

View File

@@ -29,8 +29,11 @@ import sonia.scm.repository.RepositoryDAO;
import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.StoreDecoratorFactory; import sonia.scm.store.StoreDecoratorFactory;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import static java.util.Collections.emptySet;
public class AuditLogConfigurationStoreDecorator<T> implements ConfigurationStore<T> { public class AuditLogConfigurationStoreDecorator<T> implements ConfigurationStore<T> {
private final Set<Auditor> auditors; private final Set<Auditor> auditors;
@@ -50,13 +53,40 @@ public class AuditLogConfigurationStoreDecorator<T> implements ConfigurationStor
} }
public void set(T object) { public void set(T object) {
String repositoryId = context.getStoreParameters().getRepositoryId(); if (!shouldBeIgnored(object)) {
if (!Strings.isNullOrEmpty(repositoryId)) { auditors.forEach(s -> s.createEntry(createEntryCreationContext(object)));
String name = repositoryDAO.get(repositoryId).getNamespaceAndName().toString(); }
auditors.forEach(s -> s.createEntry(new EntryCreationContext<>(object, get(), name, Set.of("repository")))); delegate.set(object);
} else { }
auditors.forEach(s -> s.createEntry(new EntryCreationContext<>(object, get(), "", Set.of(context.getStoreParameters().getName()))));
} private EntryCreationContext<T> createEntryCreationContext(T object) {
delegate.set(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));
} }
} }

View File

@@ -29,6 +29,7 @@ package sonia.scm.security;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import sonia.scm.auditlog.AuditEntry; import sonia.scm.auditlog.AuditEntry;
import sonia.scm.auditlog.AuditLogEntity;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
@@ -47,7 +48,7 @@ import java.io.Serializable;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "assigned-permission") @XmlRootElement(name = "assigned-permission")
@AuditEntry(labels = "permission") @AuditEntry(labels = "permission")
public class AssignedPermission implements PermissionObject, Serializable public class AssignedPermission implements PermissionObject, Serializable, AuditLogEntity
{ {
/** serial version uid */ /** serial version uid */
@@ -207,4 +208,9 @@ public class AssignedPermission implements PermissionObject, Serializable
/** string representation of the permission */ /** string representation of the permission */
private PermissionDescriptor permission; private PermissionDescriptor permission;
@Override
public String getEntityName() {
return getName();
}
} }

View File

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

View File

@@ -24,6 +24,7 @@
package sonia.scm.group.xml; package sonia.scm.group.xml;
import sonia.scm.auditlog.AuditEntry;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.xml.XmlDatabase; import sonia.scm.xml.XmlDatabase;
@@ -40,6 +41,7 @@ import java.util.TreeMap;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@AuditEntry(ignore = true)
@XmlRootElement(name = "group-db") @XmlRootElement(name = "group-db")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class XmlGroupDatabase implements XmlDatabase<Group> public class XmlGroupDatabase implements XmlDatabase<Group>

View File

@@ -24,6 +24,7 @@
package sonia.scm.repository.xml; package sonia.scm.repository.xml;
import sonia.scm.auditlog.AuditEntry;
import sonia.scm.repository.RepositoryRole; import sonia.scm.repository.RepositoryRole;
import sonia.scm.xml.XmlDatabase; import sonia.scm.xml.XmlDatabase;
@@ -36,6 +37,7 @@ import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
@AuditEntry(ignore = true)
@XmlRootElement(name = "user-db") @XmlRootElement(name = "user-db")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class XmlRepositoryRoleDatabase implements XmlDatabase<RepositoryRole> { public class XmlRepositoryRoleDatabase implements XmlDatabase<RepositoryRole> {

View File

@@ -24,6 +24,7 @@
package sonia.scm.user.xml; package sonia.scm.user.xml;
import sonia.scm.auditlog.AuditEntry;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.xml.XmlDatabase; import sonia.scm.xml.XmlDatabase;
@@ -40,6 +41,7 @@ import java.util.TreeMap;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@AuditEntry(ignore = true)
@XmlRootElement(name = "user-db") @XmlRootElement(name = "user-db")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class XmlUserDatabase implements XmlDatabase<User> public class XmlUserDatabase implements XmlDatabase<User>

View File

@@ -36,7 +36,7 @@ type Props = {
const Select = React.forwardRef<HTMLSelectElement, Props>( const Select = React.forwardRef<HTMLSelectElement, Props>(
({ variant, children, className, options, testId, ...props }, ref) => ( ({ variant, children, className, options, testId, ...props }, ref) => (
<div className={classNames("select", { "is-multiple": props.multiple }, createVariantClass(variant), className)}> <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
? options.map((option) => ( ? options.map((option) => (
<option {...option} key={option.value as Key}> <option {...option} key={option.value as Key}>

View File

@@ -24,6 +24,7 @@
package sonia.scm.group; package sonia.scm.group;
import sonia.scm.auditlog.AuditEntry;
import sonia.scm.xml.XmlMapMultiStringAdapter; import sonia.scm.xml.XmlMapMultiStringAdapter;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
@@ -36,6 +37,7 @@ import java.util.Set;
import static java.util.Collections.emptySet; import static java.util.Collections.emptySet;
@AuditEntry(ignore = true)
@XmlRootElement(name = "user-group-cache") @XmlRootElement(name = "user-group-cache")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
class UserGroupCache { class UserGroupCache {

View File

@@ -36,6 +36,9 @@ import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.HandlerEventType; 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.event.ScmEventBus;
import sonia.scm.group.GroupEvent; import sonia.scm.group.GroupEvent;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
@@ -52,19 +55,18 @@ import javax.xml.bind.annotation.XmlRootElement;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
/** /**
* TODO add events
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 1.31 * @since 1.31
*/ */
@@ -76,19 +78,23 @@ public class DefaultSecuritySystem implements SecuritySystem {
private static final String PERMISSION_DESCRIPTOR = private static final String PERMISSION_DESCRIPTOR =
"META-INF/scm/permissions.xml"; "META-INF/scm/permissions.xml";
/**
* the logger for DefaultSecuritySystem
*/
private static final Logger logger = private static final Logger logger =
LoggerFactory.getLogger(DefaultSecuritySystem.class); LoggerFactory.getLogger(DefaultSecuritySystem.class);
private final ConfigurationEntryStore<AssignedPermission> store;
private final ImmutableSet<PermissionDescriptor> availablePermissions;
private final Set<Auditor> auditors;
@Inject @Inject
public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory, PluginLoader pluginLoader) { public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory, PluginLoader pluginLoader, Set<Auditor> auditors) {
store = storeFactory store = storeFactory
.withType(AssignedPermission.class) .withType(AssignedPermission.class)
.withName(NAME) .withName(NAME)
.build(); .build();
this.availablePermissions = readAvailablePermissions(pluginLoader); this.availablePermissions = readAvailablePermissions(pluginLoader);
this.auditors = auditors;
} }
@Override @Override
@@ -96,9 +102,9 @@ public class DefaultSecuritySystem implements SecuritySystem {
assertHasPermission(); assertHasPermission();
validatePermission(permission); validatePermission(permission);
String id = store.put(permission); callAuditors(null, permission);
StoredAssignedPermission sap = new StoredAssignedPermission(id, permission); store.put(permission);
//J- //J-
ScmEventBus.getInstance().post( ScmEventBus.getInstance().post(
@@ -114,6 +120,7 @@ public class DefaultSecuritySystem implements SecuritySystem {
&& Objects.equal(sap.isGroupPermission(), permission.isGroupPermission()) && Objects.equal(sap.isGroupPermission(), permission.isGroupPermission())
&& Objects.equal(sap.getPermission(), permission.getPermission())); && Objects.equal(sap.getPermission(), permission.getPermission()));
if (deleted) { if (deleted) {
callAuditors(permission, null);
ScmEventBus.getInstance().post( ScmEventBus.getInstance().post(
new AssignedPermissionEvent(HandlerEventType.DELETE, permission) new AssignedPermissionEvent(HandlerEventType.DELETE, permission)
); );
@@ -170,10 +177,9 @@ public class DefaultSecuritySystem implements SecuritySystem {
return !toRemove.isEmpty(); return !toRemove.isEmpty();
} }
@SuppressWarnings("unchecked")
private static List<PermissionDescriptor> parsePermissionDescriptor( private static List<PermissionDescriptor> parsePermissionDescriptor(
JAXBContext context, URL descriptorUrl) { JAXBContext context, URL descriptorUrl) {
List<PermissionDescriptor> descriptors = Collections.EMPTY_LIST; List<PermissionDescriptor> descriptors = emptyList();
try { try {
PermissionDescriptors descriptorWrapper = PermissionDescriptors descriptorWrapper =
@@ -227,6 +233,15 @@ public class DefaultSecuritySystem implements SecuritySystem {
"permission is required"); "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. * Descriptor for permissions.
*/ */
@@ -234,10 +249,9 @@ public class DefaultSecuritySystem implements SecuritySystem {
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
private static class PermissionDescriptors { private static class PermissionDescriptors {
@SuppressWarnings("unchecked")
public List<PermissionDescriptor> getPermissions() { public List<PermissionDescriptor> getPermissions() {
if (permissions == null) { if (permissions == null) {
permissions = Collections.EMPTY_LIST; permissions = emptyList();
} }
return permissions; return permissions;
@@ -246,8 +260,4 @@ public class DefaultSecuritySystem implements SecuritySystem {
@XmlElement(name = "permission") @XmlElement(name = "permission")
private List<PermissionDescriptor> permissions; private List<PermissionDescriptor> permissions;
} }
private final ConfigurationEntryStore<AssignedPermission> store;
private final ImmutableSet<PermissionDescriptor> availablePermissions;
} }

View File

@@ -94,7 +94,7 @@ public class PermissionAssigner {
permissions.stream() permissions.stream()
.filter(permissionExists(availablePermissions, existingPermissions)) .filter(permissionExists(availablePermissions, existingPermissions))
.map(p -> new AssignedPermission(id, groupPermission, p)) .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); .forEach(securitySystem::addPermission);
} }

View File

@@ -30,21 +30,26 @@ import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm; import org.apache.shiro.realm.SimpleAccountRealm;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import sonia.scm.AbstractTestBase; import sonia.scm.AbstractTestBase;
import sonia.scm.auditlog.Auditor;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory; import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import sonia.scm.util.ClassLoaders; import sonia.scm.util.ClassLoaders;
import sonia.scm.util.MockUtil; import sonia.scm.util.MockUtil;
import java.util.Collection; import java.util.Collection;
import java.util.Set;
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.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
@@ -56,14 +61,11 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
private JAXBConfigurationEntryStoreFactory jaxbConfigurationEntryStoreFactory; private JAXBConfigurationEntryStoreFactory jaxbConfigurationEntryStoreFactory;
private PluginLoader pluginLoader; private PluginLoader pluginLoader;
@InjectMocks @Mock
private Auditor auditor;
private DefaultSecuritySystem securitySystem; private DefaultSecuritySystem securitySystem;
/**
* Method description
*
*/
@Before @Before
public void createSecuritySystem() public void createSecuritySystem()
{ {
@@ -73,12 +75,9 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
securitySystem = new DefaultSecuritySystem(jaxbConfigurationEntryStoreFactory, pluginLoader, Set.of(auditor));
} }
/**
* Method description
*
*/
@Test @Test
public void testAddPermission() public void testAddPermission()
{ {
@@ -91,10 +90,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
assertEquals(false, sap.isGroupPermission()); assertEquals(false, sap.isGroupPermission());
} }
/**
* Method description
*
*/
@Test @Test
public void testAvailablePermissions() public void testAvailablePermissions()
{ {
@@ -106,10 +101,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
assertThat(list).isNotEmpty(); assertThat(list).isNotEmpty();
} }
/**
* Method description
*
*/
@Test @Test
public void testDeletePermission() public void testDeletePermission()
{ {
@@ -123,10 +114,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
assertThat(securitySystem.getPermissions(p -> p.getName().equals("trillian"))).isEmpty(); assertThat(securitySystem.getPermissions(p -> p.getName().equals("trillian"))).isEmpty();
} }
/**
* Method description
*
*/
@Test @Test
public void testGetAllPermissions() public void testGetAllPermissions()
{ {
@@ -145,10 +132,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
assertThat(all).contains(trillian, dent, marvin); assertThat(all).contains(trillian, dent, marvin);
} }
/**
* Method description
*
*/
@Test @Test
public void testGetPermission() public void testGetPermission()
{ {
@@ -162,10 +145,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
assertThat(other).containsExactly(sap); assertThat(other).containsExactly(sap);
} }
/**
* Method description
*
*/
@Test @Test
public void testGetPermissionsWithPredicate() public void testGetPermissionsWithPredicate()
{ {
@@ -186,10 +165,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
.contains(trillian, dent); .contains(trillian, dent);
} }
/**
* Method description
*
*/
@Test(expected = UnauthorizedException.class) @Test(expected = UnauthorizedException.class)
public void testUnauthorizedAddPermission() public void testUnauthorizedAddPermission()
{ {
@@ -197,10 +172,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
createPermission("trillian", false, "repository:*:READ"); createPermission("trillian", false, "repository:*:READ");
} }
/**
* Method description
*
*/
@Test(expected = UnauthorizedException.class) @Test(expected = UnauthorizedException.class)
public void testUnauthorizedDeletePermission() public void testUnauthorizedDeletePermission()
{ {
@@ -213,16 +184,94 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
securitySystem.deletePermission(sap); securitySystem.deletePermission(sap);
} }
/** @Test
* Method description public void shouldCallAuditorForNewUserPermission()
* {
* setAdminSubject();
* @param name
* @param groupPermission createPermission("trillian", false, "repository:*:READ");
* @param value
* verify(auditor).createEntry(argThat(
* @return 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, private AssignedPermission createPermission(String name,
boolean groupPermission, String value) 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")); && Objects.equal(value, permission.getPermission().getValue())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found"));
} }
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*/
private void setAdminSubject() private void setAdminSubject()
{ {
setSubject(MockUtil.createAdminSubject()); setSubject(MockUtil.createAdminSubject());
} }
/**
* Method description
*
*/
private void setUserSubject() private void setUserSubject()
{ {
org.apache.shiro.mgt.SecurityManager sm = org.apache.shiro.mgt.SecurityManager sm =

View File

@@ -27,22 +27,28 @@ package sonia.scm.security;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.UnauthorizedException;
import org.assertj.core.api.Assertions;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.auditlog.Auditor;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
import sonia.scm.util.ClassLoaders; import sonia.scm.util.ClassLoaders;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.util.Arrays.asList; 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.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini", username = "dent", password = "secret") @SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini", username = "dent", password = "secret")
@@ -57,12 +63,14 @@ public class PermissionAssignerTest {
private DefaultSecuritySystem securitySystem; private DefaultSecuritySystem securitySystem;
private PermissionAssigner permissionAssigner; private PermissionAssigner permissionAssigner;
private Auditor auditor = mock(Auditor.class);
@Before @Before
public void init() { public void init() {
PluginLoader pluginLoader = mock(PluginLoader.class); PluginLoader pluginLoader = mock(PluginLoader.class);
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.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 @Override
public Collection<PermissionDescriptor> getAvailablePermissions() { public Collection<PermissionDescriptor> getAvailablePermissions() {
return Arrays.stream(new String[]{"perm:read:1", "perm:read:2", "perm:read:3", "perm:read:4"}) 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() { public void shouldFindUserPermissions() {
Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("1"); Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("1");
Assertions.assertThat(permissionDescriptors).hasSize(2); assertThat(permissionDescriptors).hasSize(2);
} }
@Test @Test
public void shouldFindGroupPermissions() { public void shouldFindGroupPermissions() {
Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("1"); Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("1");
Assertions.assertThat(permissionDescriptors).hasSize(2); assertThat(permissionDescriptors).hasSize(2);
} }
@Test @Test
@@ -110,7 +118,7 @@ public class PermissionAssignerTest {
Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("2"); Collection<PermissionDescriptor> permissionDescriptors = permissionAssigner.readPermissionsForUser("2");
Assertions.assertThat(permissionDescriptors).hasSize(2); assertThat(permissionDescriptors).hasSize(2);
} }
@Test @Test
@@ -132,5 +140,47 @@ public class PermissionAssignerTest {
securitySystem.addPermission(new AssignedPermission("2", "perm:read:5")); securitySystem.addPermission(new AssignedPermission("2", "perm:read:5"));
permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("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;
}
));
} }
} }