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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 =

View File

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