From 56265be9a28fc82863e15b29f739ecdd40825e19 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 9 Mar 2023 11:25:33 +0100 Subject: [PATCH] Add initial audit log API Introduce audit log API which logs all creations, modifications and deletions of annotated entities and everything which is stored inside a ConfigurationStore. Without the related Audit Log Plugin installed this API does nothing. --- gradle/changelog/audit_log.yaml | 2 + .../java/sonia/scm/auditlog/AuditEntry.java | 38 ++++ .../AuditLogConfigurationStoreDecorator.java | 62 +++++ ...LogConfigurationStoreDecoratorFactory.java | 49 ++++ .../sonia/scm/auditlog/AuditLogEntity.java | 29 +++ .../main/java/sonia/scm/auditlog/Auditor.java | 32 +++ .../scm/auditlog/EntryCreationContext.java | 54 +++++ .../sonia/scm/config/ScmConfiguration.java | 2 + .../src/main/java/sonia/scm/group/Group.java | 212 +++++++----------- .../java/sonia/scm/repository/Namespace.java | 2 + .../java/sonia/scm/repository/Repository.java | 14 +- .../sonia/scm/repository/RepositoryRole.java | 12 +- .../scm/security/AssignedPermission.java | 2 + .../ConfigurationStoreDecoratorFactory.java | 29 +++ .../scm/store/StoreDecoratorFactory.java | 41 ++++ .../src/main/java/sonia/scm/user/User.java | 14 +- .../store/JAXBConfigurationStoreFactory.java | 18 +- .../scm/store/JAXBConfigurationStoreTest.java | 6 +- .../java/sonia/scm/repository/GitConfig.java | 2 + .../scm/repository/GitRepositoryConfig.java | 3 + .../java/sonia/scm/repository/HgConfig.java | 2 + .../sonia/scm/repository/HgGlobalConfig.java | 2 + .../scm/repository/HgRepositoryConfig.java | 2 + .../java/sonia/scm/repository/SvnConfig.java | 3 + .../java/sonia/scm/ManagerDaoAdapter.java | 20 +- .../sonia/scm/group/DefaultGroupManager.java | 14 +- .../lifecycle/modules/BootstrapModule.java | 14 ++ .../lifecycle/modules/ScmServletModule.java | 6 + .../repository/DefaultRepositoryManager.java | 13 +- .../DefaultRepositoryRoleManager.java | 33 +-- .../sonia/scm/user/DefaultUserManager.java | 6 +- .../DefaultRepositoryManagerPerfTest.java | 3 +- .../DefaultRepositoryManagerTest.java | 6 +- .../DefaultRepositoryRoleManagerTest.java | 9 +- .../DefaultMigrationStrategyDAOTest.java | 3 +- .../scm/user/DefaultUserManagerTest.java | 9 +- 36 files changed, 590 insertions(+), 178 deletions(-) create mode 100644 gradle/changelog/audit_log.yaml create mode 100644 scm-core/src/main/java/sonia/scm/auditlog/AuditEntry.java create mode 100644 scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecorator.java create mode 100644 scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecoratorFactory.java create mode 100644 scm-core/src/main/java/sonia/scm/auditlog/AuditLogEntity.java create mode 100644 scm-core/src/main/java/sonia/scm/auditlog/Auditor.java create mode 100644 scm-core/src/main/java/sonia/scm/auditlog/EntryCreationContext.java create mode 100644 scm-core/src/main/java/sonia/scm/store/ConfigurationStoreDecoratorFactory.java create mode 100644 scm-core/src/main/java/sonia/scm/store/StoreDecoratorFactory.java diff --git a/gradle/changelog/audit_log.yaml b/gradle/changelog/audit_log.yaml new file mode 100644 index 0000000000..d5c38f6180 --- /dev/null +++ b/gradle/changelog/audit_log.yaml @@ -0,0 +1,2 @@ +- type: added + description: Initial implementation of an audit log API diff --git a/scm-core/src/main/java/sonia/scm/auditlog/AuditEntry.java b/scm-core/src/main/java/sonia/scm/auditlog/AuditEntry.java new file mode 100644 index 0000000000..2dd9cd729d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/auditlog/AuditEntry.java @@ -0,0 +1,38 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.LOCAL_VARIABLE, ElementType.FIELD, ElementType.PACKAGE, ElementType.METHOD}) +public @interface AuditEntry { + String[] labels() default {}; + String[] maskedFields() default {}; + String[] ignoredFields() default {}; +} diff --git a/scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecorator.java b/scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecorator.java new file mode 100644 index 0000000000..9b21502796 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecorator.java @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.auditlog; + +import com.google.common.base.Strings; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.StoreDecoratorFactory; + +import java.util.Set; + +public class AuditLogConfigurationStoreDecorator implements ConfigurationStore { + + private final Set auditors; + private final RepositoryDAO repositoryDAO; + private final ConfigurationStore delegate; + private final StoreDecoratorFactory.Context context; + + public AuditLogConfigurationStoreDecorator(Set auditors, RepositoryDAO repositoryDAO, ConfigurationStore delegate, StoreDecoratorFactory.Context context) { + this.auditors = auditors; + this.repositoryDAO = repositoryDAO; + this.delegate = delegate; + this.context = context; + } + + public T get() { + return delegate.get(); + } + + 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); + } +} diff --git a/scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecoratorFactory.java b/scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecoratorFactory.java new file mode 100644 index 0000000000..3b475def5d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/auditlog/AuditLogConfigurationStoreDecoratorFactory.java @@ -0,0 +1,49 @@ +/* + * 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 sonia.scm.repository.RepositoryDAO; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreDecoratorFactory; + +import javax.inject.Inject; +import java.util.Set; + +public class AuditLogConfigurationStoreDecoratorFactory implements ConfigurationStoreDecoratorFactory { + + private final Set auditors; + private final RepositoryDAO repositoryDAO; + + @Inject + public AuditLogConfigurationStoreDecoratorFactory(Set auditor, RepositoryDAO repositoryDAO) { + this.auditors = auditor; + this.repositoryDAO = repositoryDAO; + } + + @Override + public ConfigurationStore createDecorator(ConfigurationStore object, Context context) { + return new AuditLogConfigurationStoreDecorator<>(auditors, repositoryDAO, object, context); + } +} diff --git a/scm-core/src/main/java/sonia/scm/auditlog/AuditLogEntity.java b/scm-core/src/main/java/sonia/scm/auditlog/AuditLogEntity.java new file mode 100644 index 0000000000..d79fd86e7a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/auditlog/AuditLogEntity.java @@ -0,0 +1,29 @@ +/* + * 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; + +public interface AuditLogEntity { + String getEntityName(); +} diff --git a/scm-core/src/main/java/sonia/scm/auditlog/Auditor.java b/scm-core/src/main/java/sonia/scm/auditlog/Auditor.java new file mode 100644 index 0000000000..0715d0fbf4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/auditlog/Auditor.java @@ -0,0 +1,32 @@ +/* + * 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 sonia.scm.plugin.ExtensionPoint; + +@ExtensionPoint +public interface Auditor { + void createEntry(EntryCreationContext context); +} diff --git a/scm-core/src/main/java/sonia/scm/auditlog/EntryCreationContext.java b/scm-core/src/main/java/sonia/scm/auditlog/EntryCreationContext.java new file mode 100644 index 0000000000..ca06ebe6eb --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/auditlog/EntryCreationContext.java @@ -0,0 +1,54 @@ +/* + * 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 lombok.Getter; + +import java.util.Set; + +import static java.util.Collections.emptySet; + +@Getter +public class EntryCreationContext { + private final T object; + private final T oldObject; + private final String entity; + private final Set additionalLabels; + + public EntryCreationContext(T object, T oldObject) { + this(object, oldObject, "", emptySet()); + } + + public EntryCreationContext(T object, T oldObject, Set additionalLabels) { + this(object, oldObject, "", additionalLabels); + } + + public EntryCreationContext(T object, T oldObject, String entity, Set additionalLabels) { + this.object = object; + this.oldObject = oldObject; + this.entity = entity; + this.additionalLabels = additionalLabels; + } +} diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 3508b5ad92..41082ce50c 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -29,6 +29,7 @@ import com.google.common.collect.Sets; import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.auditlog.AuditEntry; import sonia.scm.event.ScmEventBus; import sonia.scm.security.AnonymousMode; import sonia.scm.util.HttpUtil; @@ -57,6 +58,7 @@ import java.util.concurrent.TimeUnit; @Singleton @XmlRootElement(name = "scm-config") @XmlAccessorType(XmlAccessType.FIELD) +@AuditEntry(labels = "config", maskedFields = "proxyPassword") public class ScmConfiguration implements Configuration { /** diff --git a/scm-core/src/main/java/sonia/scm/group/Group.java b/scm-core/src/main/java/sonia/scm/group/Group.java index 83a07a5af0..36e868bb7e 100644 --- a/scm-core/src/main/java/sonia/scm/group/Group.java +++ b/scm-core/src/main/java/sonia/scm/group/Group.java @@ -34,6 +34,8 @@ import com.google.common.collect.Lists; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; import sonia.scm.ReducedModelObject; +import sonia.scm.auditlog.AuditEntry; +import sonia.scm.auditlog.AuditLogEntity; import sonia.scm.search.Indexed; import sonia.scm.search.IndexedType; import sonia.scm.util.Util; @@ -50,7 +52,7 @@ import java.util.List; /** * Organizes users into a group for easier permissions management. - * + *

* TODO for 2.0: Use a set instead of a list for members * * @author Sebastian Sdorra @@ -63,31 +65,30 @@ import java.util.List; ) @XmlRootElement(name = "groups") @XmlAccessorType(XmlAccessType.FIELD) +@AuditEntry(labels = "group", ignoredFields = "lastModified") public class Group extends BasicPropertiesAware - implements ModelObject, PermissionObject, ReducedModelObject -{ + implements ModelObject, PermissionObject, ReducedModelObject, AuditLogEntity { - /** Field description */ + /** + * Field description + */ private static final long serialVersionUID = 1752369869345245872L; //~--- constructors --------------------------------------------------------- /** * Constructs {@link Group} object. This constructor is required by JAXB. - * */ - public Group() {} + public Group() { + } /** * Constructs {@link Group} object. * - * - * * @param type of the group * @param name of the group */ - public Group(String type, String name) - { + public Group(String type, String name) { this.type = type; this.name = name; this.members = Lists.newArrayList(); @@ -96,14 +97,11 @@ public class Group extends BasicPropertiesAware /** * Constructs {@link Group} object. * - * - * - * @param type of the group - * @param name of the group + * @param type of the group + * @param name of the group * @param members of the groups */ - public Group(String type, String name, List members) - { + public Group(String type, String name, List members) { this.type = type; this.name = name; this.members = members; @@ -112,20 +110,16 @@ public class Group extends BasicPropertiesAware /** * Constructs {@link Group} object. * - * - * - * @param type of the group - * @param name of the group + * @param type of the group + * @param name of the group * @param members of the groups */ - public Group(String type, String name, String... members) - { + public Group(String type, String name, String... members) { this.type = type; this.name = name; this.members = Lists.newArrayList(); - if (Util.isNotEmpty(members)) - { + if (Util.isNotEmpty(members)) { this.members.addAll(Arrays.asList(members)); } } @@ -135,43 +129,32 @@ public class Group extends BasicPropertiesAware /** * Add a new member to the group. * - * * @param member - The name of new group member - * * @return true if the operation was successful */ - public boolean add(String member) - { + public boolean add(String member) { return getMembers().add(member); } /** * Remove all members of the group. - * */ - public void clear() - { + public void clear() { members.clear(); } /** * Returns a clone of the group. * - * * @return a clone of the group - * */ @Override - public Group clone() - { + public Group clone() { Group group = null; - try - { + try { group = (Group) super.clone(); - } - catch (CloneNotSupportedException ex) - { + } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } @@ -181,11 +164,9 @@ public class Group extends BasicPropertiesAware /** * Copies all properties of this group to the given one. * - * * @param group to copies all properties of this one */ - public void copyProperties(Group group) - { + public void copyProperties(Group group) { group.setName(name); group.setMembers(members); group.setType(type); @@ -196,21 +177,16 @@ public class Group extends BasicPropertiesAware /** * Returns true if this {@link Group} is the same as the obj argument. * - * * @param obj - the reference object with which to compare - * * @return true if this {@link Group} is the same as the obj argument */ @Override - public boolean equals(Object obj) - { - if (obj == null) - { + public boolean equals(Object obj) { + if (obj == null) { return false; } - if (getClass() != obj.getClass()) - { + if (getClass() != obj.getClass()) { return false; } @@ -229,12 +205,10 @@ public class Group extends BasicPropertiesAware /** * Returns a hash code value for this {@link Group}. * - * * @return a hash code value for this {@link Group} */ @Override - public int hashCode() - { + public int hashCode() { return Objects.hashCode(name, description, members, type, creationDate, lastModified, properties); } @@ -242,36 +216,31 @@ public class Group extends BasicPropertiesAware /** * Remove the given member from this group. * - * * @param member to remove from this group - * * @return true if the operation was successful */ - public boolean remove(String member) - { + public boolean remove(String member) { return members.remove(member); } /** * Returns a {@link String} that represents this group. * - * * @return a {@link String} that represents this group */ @Override - public String toString() - { + public String toString() { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("description", description) - .add("members", members) - .add("type", type) - .add("external", external) - .add("creationDate", creationDate) - .add("lastModified", lastModified) - .add("properties", properties) - .toString(); + .add("name", name) + .add("description", description) + .add("members", members) + .add("type", type) + .add("external", external) + .add("creationDate", creationDate) + .add("lastModified", lastModified) + .add("properties", properties) + .toString(); //J+ } @@ -280,22 +249,18 @@ public class Group extends BasicPropertiesAware /** * Returns a timestamp of the creation date of this group. * - * * @return a timestamp of the creation date of this group */ - public Long getCreationDate() - { + public Long getCreationDate() { return creationDate; } /** * Returns the description of this group. * - * - * @return the description of this group + * @return the description of this group */ - public String getDescription() - { + public String getDescription() { return description; } @@ -303,12 +268,10 @@ public class Group extends BasicPropertiesAware * Returns the unique name of this group. This method is an alias for the * {@link #getName()} method. * - * * @return the unique name of this group */ @Override - public String getId() - { + public String getId() { return name; } @@ -320,23 +283,19 @@ public class Group extends BasicPropertiesAware /** * Returns a timestamp of the last modified date of this group. * - * * @return a timestamp of the last modified date of this group */ @Override - public Long getLastModified() - { + public Long getLastModified() { return lastModified; } /** * Returns a {@link java.util.List} of all members of this group. * - * * @return a {@link java.util.List} of all members of this group */ - public List getMembers() - { + public List getMembers() { if (external) { return Collections.emptyList(); } else if (members == null) { @@ -349,23 +308,19 @@ public class Group extends BasicPropertiesAware /** * Returns the unique name of this group. * - * * @return the unique name of this group */ - public String getName() - { + public String getName() { return name; } /** * Returns the type of this group. The default type is xml. * - * * @return the type of this group */ @Override - public String getType() - { + public String getType() { return type; } @@ -381,25 +336,20 @@ public class Group extends BasicPropertiesAware /** * Returns true if the member is a member of this group. * - * * @param member - The name of the member - * * @return true if the member is a member of this group */ - public boolean isMember(String member) - { + public boolean isMember(String member) { return (members != null) && members.contains(member); } /** * Returns true if the group is valid. * - * * @return true if the group is valid */ @Override - public boolean isValid() - { + public boolean isValid() { return ValidationUtil.isNameValid(name) && Util.isNotEmpty(type); } @@ -408,66 +358,54 @@ public class Group extends BasicPropertiesAware /** * Sets the date the group was created. * - * * @param creationDate - date the group was last modified */ - public void setCreationDate(Long creationDate) - { + public void setCreationDate(Long creationDate) { this.creationDate = creationDate; } /** * Sets the description of the group. * - * * @param description of the group */ - public void setDescription(String description) - { + public void setDescription(String description) { this.description = description; } /** * Sets the date the group was last modified. * - * * @param lastModified - date the group was last modified */ - public void setLastModified(Long lastModified) - { + public void setLastModified(Long lastModified) { this.lastModified = lastModified; } /** * Sets the members of the group. * - * * @param members of the group */ - public void setMembers(List members) - { + public void setMembers(List members) { this.members = members; } /** * Sets the name of the group. * - * * @param name of the group */ - public void setName(String name) - { + public void setName(String name) { this.name = name; } /** * Sets the type of the group. * - * * @param type of the group */ - public void setType(String type) - { + public void setType(String type) { this.type = type; } @@ -476,35 +414,57 @@ public class Group extends BasicPropertiesAware * * @param {@code true} for a external group */ - public void setExternal(boolean external) - { + public void setExternal(boolean external) { this.external = external; } //~--- fields --------------------------------------------------------------- - /** external group */ + /** + * external group + */ private boolean external = false; - /** timestamp of the creation date of this group */ + /** + * timestamp of the creation date of this group + */ @Indexed private Long creationDate; - /** description of this group */ + /** + * description of this group + */ @Indexed(defaultQuery = true, highlighted = true) private String description; - /** timestamp of the last modified date of this group */ + /** + * timestamp of the last modified date of this group + */ @Indexed private Long lastModified; - /** members of this group */ + /** + * members of this group + */ private List members; - /** name of this group */ + /** + * name of this group + */ @Indexed(defaultQuery = true, boost = 1.5f) private String name; - /** type of this group */ + /** + * type of this group + */ private String type; + + /** + * Get the entity name which is used for the audit log + * @since 2.43.0 + */ + @Override + public String getEntityName() { + return getName(); + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/Namespace.java b/scm-core/src/main/java/sonia/scm/repository/Namespace.java index bfdad435b7..d25a2f0f7c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Namespace.java +++ b/scm-core/src/main/java/sonia/scm/repository/Namespace.java @@ -28,6 +28,7 @@ import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.StaticPermissions; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; +import sonia.scm.auditlog.AuditEntry; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -46,6 +47,7 @@ import static java.util.Collections.unmodifiableCollection; ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "namespaces") +@AuditEntry(labels = "namespace") public class Namespace implements PermissionObject, Cloneable, RepositoryPermissionHolder { private String namespace; diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index c5f51bef2c..6997b31f44 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -31,6 +31,8 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; +import sonia.scm.auditlog.AuditEntry; +import sonia.scm.auditlog.AuditLogEntity; import sonia.scm.search.Indexed; import sonia.scm.search.IndexedType; import sonia.scm.util.Util; @@ -64,9 +66,10 @@ import java.util.Set; @Guard(guard = RepositoryPermissionGuard.class) } ) +@AuditEntry(labels = "repository", ignoredFields = "lastModified") public class Repository extends BasicPropertiesAware - implements ModelObject, PermissionObject, RepositoryCoordinates, RepositoryPermissionHolder { + implements ModelObject, PermissionObject, RepositoryCoordinates, RepositoryPermissionHolder, AuditLogEntity { private static final long serialVersionUID = 3486560714961909711L; @@ -413,4 +416,13 @@ public class Repository .add("healthCheckFailures", healthCheckFailures) .toString(); } + + /** + * Get the entity name which is used for the audit log + * @since 2.43.0 + */ + @Override + public String getEntityName() { + return getNamespaceAndName().toString(); + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java index 534e18b971..3aca190a42 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import com.github.sdorra.ssp.PermissionObject; @@ -30,6 +30,8 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Strings; import sonia.scm.ModelObject; +import sonia.scm.auditlog.AuditEntry; +import sonia.scm.auditlog.AuditLogEntity; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -51,7 +53,8 @@ import static java.util.Collections.unmodifiableSet; @StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"write"}) @XmlRootElement(name = "roles") @XmlAccessorType(XmlAccessType.FIELD) -public class RepositoryRole implements ModelObject, PermissionObject { +@AuditEntry(labels = "role", ignoredFields = "lastModified") +public class RepositoryRole implements ModelObject, PermissionObject, AuditLogEntity { private static final long serialVersionUID = -723588336073192740L; @@ -218,4 +221,9 @@ public class RepositoryRole implements ModelObject, PermissionObject { throw new RuntimeException(ex); } } + + @Override + public String getEntityName() { + return getId(); + } } diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java index e7bb01acec..9ea4804c67 100644 --- a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java @@ -28,6 +28,7 @@ package sonia.scm.security; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import sonia.scm.auditlog.AuditEntry; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -45,6 +46,7 @@ import java.io.Serializable; */ @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "assigned-permission") +@AuditEntry(labels = "permission") public class AssignedPermission implements PermissionObject, Serializable { diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreDecoratorFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreDecoratorFactory.java new file mode 100644 index 0000000000..dd0e7ded3d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreDecoratorFactory.java @@ -0,0 +1,29 @@ +/* + * 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.store; + +public interface ConfigurationStoreDecoratorFactory extends StoreDecoratorFactory { + ConfigurationStore createDecorator(ConfigurationStore object, Context context); +} diff --git a/scm-core/src/main/java/sonia/scm/store/StoreDecoratorFactory.java b/scm-core/src/main/java/sonia/scm/store/StoreDecoratorFactory.java new file mode 100644 index 0000000000..df09519ca1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/StoreDecoratorFactory.java @@ -0,0 +1,41 @@ +/* + * 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.store; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +public interface StoreDecoratorFactory { + + ConfigurationStore createDecorator(ConfigurationStore object, Context context); + + @AllArgsConstructor + @Getter + class Context { + private TypedStoreParameters storeParameters; + } +} + + diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index 9ebd1ee51d..6ebe29df80 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -35,6 +35,8 @@ import lombok.Setter; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; import sonia.scm.ReducedModelObject; +import sonia.scm.auditlog.AuditEntry; +import sonia.scm.auditlog.AuditLogEntity; import sonia.scm.search.Indexed; import sonia.scm.search.IndexedType; import sonia.scm.util.Util; @@ -58,7 +60,8 @@ import java.security.Principal; @Setter @NoArgsConstructor @AllArgsConstructor -public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject { +@AuditEntry(labels = "user", maskedFields = "password", ignoredFields = "lastModified") +public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject, AuditLogEntity { private static final long serialVersionUID = -3089541936726329663L; @@ -225,4 +228,13 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject public String getId() { return name; } + + /** + * Get the entity name which is used for the audit log + * @since 2.43.0 + */ + @Override + public String getEntityName() { + return getName(); + } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java index 25f5f750e6..9c76f84f35 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java @@ -30,6 +30,8 @@ import sonia.scm.SCMContextProvider; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.repository.RepositoryReadOnlyChecker; +import java.util.Set; + /** * JAXB implementation of {@link ConfigurationStoreFactory}. * @@ -38,20 +40,24 @@ import sonia.scm.repository.RepositoryReadOnlyChecker; @Singleton public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory implements ConfigurationStoreFactory { + private final Set decoratorFactories; + /** * Constructs a new instance. * * @param repositoryLocationResolver Resolver to get the repository Directory */ @Inject - public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryReadOnlyChecker readOnlyChecker) { + public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryReadOnlyChecker readOnlyChecker, Set decoratorFactories) { super(contextProvider, repositoryLocationResolver, Store.CONFIG, readOnlyChecker); + this.decoratorFactories = decoratorFactories; } @Override - public JAXBConfigurationStore getStore(TypedStoreParameters storeParameters) { + public ConfigurationStore getStore(TypedStoreParameters storeParameters) { TypedStoreContext context = TypedStoreContext.of(storeParameters); - return new JAXBConfigurationStore<>( + + ConfigurationStore store = new JAXBConfigurationStore<>( context, storeParameters.getType(), getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), @@ -59,5 +65,11 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme storeParameters.getRepositoryId()), () -> mustBeReadOnly(storeParameters) ); + + for (ConfigurationStoreDecoratorFactory factory : decoratorFactories) { + store = factory.createDecorator(store, new StoreDecoratorFactory.Context(storeParameters)); + } + + return store; } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java index 6d4fea3816..34a4753cbb 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java @@ -25,9 +25,13 @@ package sonia.scm.store; import org.junit.Test; +import org.junit.jupiter.api.extension.ExtendWith; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryReadOnlyChecker; +import java.util.Collections; + +import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -46,7 +50,7 @@ public class JAXBConfigurationStoreTest extends StoreTestBase { @Override protected JAXBConfigurationStoreFactory createStoreFactory() { - return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker); + return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker, emptySet()); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java index 0522ff63ba..d827d97cf5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java @@ -27,6 +27,7 @@ package sonia.scm.repository; //~--- JDK imports ------------------------------------------------------------ import com.google.common.base.Strings; +import sonia.scm.auditlog.AuditEntry; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -39,6 +40,7 @@ import javax.xml.bind.annotation.XmlTransient; */ @XmlRootElement(name = "config") @XmlAccessorType(XmlAccessType.FIELD) +@AuditEntry(labels = {"git", "config"}) public class GitConfig extends RepositoryConfig { private static final String FALLBACK_BRANCH = "main"; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfig.java index e4b2b7e2f9..157c551794 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfig.java @@ -24,12 +24,15 @@ package sonia.scm.repository; +import sonia.scm.auditlog.AuditEntry; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "config") @XmlAccessorType(XmlAccessType.FIELD) +@AuditEntry(labels = {"git", "config"}) public class GitRepositoryConfig { public GitRepositoryConfig() { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java index 552c9e8b60..341c003087 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java @@ -25,10 +25,12 @@ package sonia.scm.repository; import lombok.Value; +import sonia.scm.auditlog.AuditEntry; import java.io.File; @Value +@AuditEntry(labels = {"hg", "config"}) public class HgConfig { String hgBinary; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgGlobalConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgGlobalConfig.java index a1e671e28a..708e49e3c2 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgGlobalConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgGlobalConfig.java @@ -25,6 +25,7 @@ package sonia.scm.repository; +import sonia.scm.auditlog.AuditEntry; import sonia.scm.util.Util; import javax.xml.bind.annotation.XmlRootElement; @@ -36,6 +37,7 @@ import javax.xml.bind.annotation.XmlTransient; * @author Sebastian Sdorra */ @XmlRootElement(name = "config") +@AuditEntry(labels = {"hg", "config"}) public class HgGlobalConfig extends RepositoryConfig { public static final String PERMISSION = "hg"; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryConfig.java index 4b401462db..eaf7515a84 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryConfig.java @@ -25,11 +25,13 @@ package sonia.scm.repository; import lombok.Data; +import sonia.scm.auditlog.AuditEntry; import javax.xml.bind.annotation.XmlRootElement; @Data @XmlRootElement +@AuditEntry(labels = {"hg", "config"}) public class HgRepositoryConfig { String encoding; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java index b1426eaad1..d05f5b2016 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java @@ -24,6 +24,8 @@ package sonia.scm.repository; +import sonia.scm.auditlog.AuditEntry; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; @@ -36,6 +38,7 @@ import javax.xml.bind.annotation.XmlTransient; */ @XmlRootElement(name = "config") @XmlAccessorType(XmlAccessType.FIELD) +@AuditEntry(labels = {"svn", "config"}) public class SvnConfig extends RepositoryConfig { diff --git a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java index 86399fa4af..0df15cab5b 100644 --- a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java @@ -21,12 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm; import com.github.sdorra.ssp.PermissionCheck; +import sonia.scm.auditlog.AuditEntry; +import sonia.scm.auditlog.Auditor; +import sonia.scm.auditlog.EntryCreationContext; import sonia.scm.util.AssertUtil; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -34,9 +38,11 @@ import java.util.function.Supplier; public class ManagerDaoAdapter { private final GenericDAO dao; + private final Set auditors; - public ManagerDaoAdapter(GenericDAO dao) { + public ManagerDaoAdapter(GenericDAO dao, Set auditors) { this.dao = dao; + this.auditors = auditors; } public void modify(T object, Function permissionCheck, AroundHandler beforeUpdate, AroundHandler afterUpdate) { @@ -51,6 +57,8 @@ public class ManagerDaoAdapter { object.setLastModified(System.currentTimeMillis()); object.setCreationDate(notModified.getCreationDate()); + callAuditors(notModified, object); + dao.modify(object); afterUpdate.handle(notModified); @@ -73,6 +81,7 @@ public class ManagerDaoAdapter { existsCheck.accept(newObject); newObject.setCreationDate(System.currentTimeMillis()); beforeCreate.handle(newObject); + callAuditors(null, newObject); dao.add(newObject); afterCreate.handle(newObject); return newObject; @@ -82,6 +91,7 @@ public class ManagerDaoAdapter { permissionCheck.get().check(); if (dao.contains(toDelete)) { beforeDelete.handle(toDelete); + callAuditors(toDelete, null); dao.delete(toDelete); afterDelete.handle(toDelete); } else { @@ -89,6 +99,12 @@ public class ManagerDaoAdapter { } } + private void callAuditors(T notModified, T newObject) { + if ((newObject == null? notModified: newObject).getClass().isAnnotationPresent(AuditEntry.class)) { + auditors.forEach(s -> s.createEntry(new EntryCreationContext<>(newObject, notModified))); + } + } + @FunctionalInterface public interface AroundHandler { void handle(T notModified); diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index ec83256af8..bcad3b68d8 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -37,6 +37,7 @@ import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; import sonia.scm.NotFoundException; import sonia.scm.SCMContextProvider; +import sonia.scm.auditlog.Auditor; import sonia.scm.search.SearchRequest; import sonia.scm.search.SearchUtil; import sonia.scm.util.CollectionAppender; @@ -77,10 +78,10 @@ public class DefaultGroupManager extends AbstractGroupManager * @param groupDAO */ @Inject - public DefaultGroupManager(GroupDAO groupDAO) + public DefaultGroupManager(GroupDAO groupDAO, Set auditors) { this.groupDAO = groupDAO; - this.managerDaoAdapter = new ManagerDaoAdapter<>(groupDAO); + this.managerDaoAdapter = new ManagerDaoAdapter<>(groupDAO, auditors); } //~--- methods -------------------------------------------------------------- @@ -285,16 +286,11 @@ public class DefaultGroupManager extends AbstractGroupManager final PermissionActionCheck check = GroupPermissions.read(); return Util.createSubCollection(groupDAO.getAll(), comparator, - new CollectionAppender() - { - @Override - public void append(Collection collection, Group group) - { + (collection, group) -> { if (check.isPermitted(group)) { collection.add(group.clone()); } - } - }, start, limit); + }, start, limit); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index cfea4befff..411a79968a 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -26,6 +26,7 @@ package sonia.scm.lifecycle.modules; import com.google.inject.AbstractModule; import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; import com.google.inject.throwingproviders.ThrowingProviderBinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +50,8 @@ import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.KeyGenerator; import sonia.scm.store.BlobStoreFactory; import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreDecoratorFactory; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.DataStoreFactory; import sonia.scm.store.DefaultBlobDirectoryAccess; @@ -100,6 +103,10 @@ public class BootstrapModule extends AbstractModule { // note CipherUtil uses an other generator bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler()); + // Bind empty set in the bootstrap module + Multibinder.newSetBinder(binder(), ConfigurationStoreDecoratorFactory.class).addBinding() + .to(NoOpConfigurationStoreDecoratorFactory.class); + // bind core bind(RepositoryArchivedCheck.class, EventDrivenRepositoryArchiveCheck.class); bind(RepositoryExportingCheck.class, DefaultRepositoryExportingCheck.class); @@ -137,4 +144,11 @@ public class BootstrapModule extends AbstractModule { return implementation; } + + private static class NoOpConfigurationStoreDecoratorFactory implements ConfigurationStoreDecoratorFactory { + @Override + public ConfigurationStore createDecorator(ConfigurationStore object, Context context) { + return object; + } + } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java index e789648ac3..ed5e8880a6 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java @@ -44,6 +44,7 @@ import sonia.scm.api.v2.resources.BranchLinkProvider; import sonia.scm.api.v2.resources.DefaultBranchLinkProvider; import sonia.scm.api.v2.resources.DefaultRepositoryLinkProvider; import sonia.scm.api.v2.resources.RepositoryLinkProvider; +import sonia.scm.auditlog.AuditLogConfigurationStoreDecoratorFactory; import sonia.scm.cache.CacheManager; import sonia.scm.cache.GuavaCacheManager; import sonia.scm.config.ScmConfiguration; @@ -98,6 +99,7 @@ import sonia.scm.security.DefaultSecuritySystem; import sonia.scm.security.LoginAttemptHandler; import sonia.scm.security.RepositoryPermissionProvider; import sonia.scm.security.SecuritySystem; +import sonia.scm.store.ConfigurationStoreDecoratorFactory; import sonia.scm.store.FileStoreExporter; import sonia.scm.store.StoreExporter; import sonia.scm.template.MustacheTemplateEngine; @@ -154,6 +156,10 @@ class ScmServletModule extends ServletModule { bind(NamespaceStrategy.class).toProvider(NamespaceStrategyProvider.class); + // bind store decorators + Multibinder storeDecoratorMultiBinder = Multibinder.newSetBinder(binder(), ConfigurationStoreDecoratorFactory.class); + storeDecoratorMultiBinder.addBinding().to(AuditLogConfigurationStoreDecoratorFactory.class); + // bind repository provider ThrowingProviderBinder.create(binder()) .bind(RepositoryProvider.class, Repository.class) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 2736094ab1..e9ffb89aa3 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -37,6 +37,7 @@ import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.SCMContextProvider; import sonia.scm.Type; +import sonia.scm.auditlog.Auditor; import sonia.scm.event.ScmEventBus; import sonia.scm.security.AuthorizationChangedEvent; import sonia.scm.security.KeyGenerator; @@ -112,9 +113,13 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private final RepositoryPostProcessor repositoryPostProcessor; @Inject - public DefaultRepositoryManager(SCMContextProvider contextProvider, KeyGenerator keyGenerator, - RepositoryDAO repositoryDAO, Set handlerSet, - Provider namespaceStrategyProvider, RepositoryPostProcessor repositoryPostProcessor) { + public DefaultRepositoryManager(SCMContextProvider contextProvider, + KeyGenerator keyGenerator, + RepositoryDAO repositoryDAO, + Set handlerSet, + Provider namespaceStrategyProvider, + RepositoryPostProcessor repositoryPostProcessor, + Set auditors) { this.keyGenerator = keyGenerator; this.repositoryDAO = repositoryDAO; this.namespaceStrategyProvider = namespaceStrategyProvider; @@ -126,7 +131,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { for (RepositoryHandler handler : handlerSet) { addHandler(contextProvider, handler); } - managerDaoAdapter = new ManagerDaoAdapter<>(repositoryDAO); + managerDaoAdapter = new ManagerDaoAdapter<>(repositoryDAO, auditors); } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java index 88364bd887..4e86063acf 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import com.google.inject.Inject; @@ -34,31 +34,35 @@ import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; import sonia.scm.NotFoundException; import sonia.scm.SCMContextProvider; +import sonia.scm.auditlog.Auditor; import sonia.scm.security.RepositoryPermissionProvider; import sonia.scm.util.Util; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; -@Singleton @EagerSingleton -public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager -{ +@Singleton +@EagerSingleton +public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager { - /** the logger for XmlRepositoryRoleManager */ + /** + * the logger for XmlRepositoryRoleManager + */ private static final Logger logger = LoggerFactory.getLogger(DefaultRepositoryRoleManager.class); @Inject - public DefaultRepositoryRoleManager(RepositoryRoleDAO repositoryRoleDAO, RepositoryPermissionProvider repositoryPermissionProvider) - { + public DefaultRepositoryRoleManager(RepositoryRoleDAO repositoryRoleDAO, + RepositoryPermissionProvider repositoryPermissionProvider, + Set auditors) { this.repositoryRoleDAO = repositoryRoleDAO; - this.managerDaoAdapter = new ManagerDaoAdapter<>(repositoryRoleDAO); + this.managerDaoAdapter = new ManagerDaoAdapter<>(repositoryRoleDAO, auditors); this.repositoryPermissionProvider = repositoryPermissionProvider; } @@ -99,6 +103,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager @Override public void init(SCMContextProvider context) { + // Nothing } @Override @@ -179,20 +184,16 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager @Override public Collection getAll(Comparator comaparator, int start, int limit) { return Util.createSubCollection(getAll(), comaparator, - (collection, item) -> { - collection.add(item.clone()); - }, start, limit); + (collection, item) -> collection.add(item.clone()), start, limit); } @Override - public Collection getAll(int start, int limit) - { + public Collection getAll(int start, int limit) { return getAll(null, start, limit); } @Override - public Long getLastModified() - { + public Long getLastModified() { return repositoryRoleDAO.getLastModified(); } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index a126beadf7..81f3d87b9a 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -37,6 +37,7 @@ import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; import sonia.scm.NotFoundException; import sonia.scm.SCMContextProvider; +import sonia.scm.auditlog.Auditor; import sonia.scm.search.SearchRequest; import sonia.scm.search.SearchUtil; import sonia.scm.security.Authentications; @@ -48,6 +49,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.function.Predicate; /** @@ -76,11 +78,11 @@ public class DefaultUserManager extends AbstractUserManager * @param userDAO */ @Inject - public DefaultUserManager(PasswordService passwordService, UserDAO userDAO) + public DefaultUserManager(PasswordService passwordService, UserDAO userDAO, Set auditors) { this.passwordService = passwordService; this.userDAO = userDAO; - this.managerDaoAdapter = new ManagerDaoAdapter<>(userDAO); + this.managerDaoAdapter = new ManagerDaoAdapter<>(userDAO, auditors); } //~--- methods -------------------------------------------------------------- diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index 8612b130b3..27ae71bd2a 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -119,7 +119,8 @@ public class DefaultRepositoryManagerPerfTest { repositoryDAO, handlerSet, Providers.of(namespaceStrategy), - repositoryPostProcessor); + repositoryPostProcessor, + Collections.emptySet()); setUpTestRepositories(); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index a34e02b9f7..dff97c98cd 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -48,6 +48,7 @@ import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.SCMContext; import sonia.scm.ScmConstraintViolationException; +import sonia.scm.TempSCMContextProvider; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContextFactory; @@ -88,9 +89,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - -import sonia.scm.TempSCMContextProvider; - //~--- JDK imports ------------------------------------------------------------ /** @@ -561,7 +559,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); return new DefaultRepositoryManager(contextProvider, - keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy), postProcessor); + keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy), postProcessor, emptySet()); } private RepositoryDAO createRepositoryDaoMock() { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java index 4f296b96ca..3a910f3ae9 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryRoleManagerTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import org.apache.shiro.authz.UnauthorizedException; @@ -48,6 +48,7 @@ import java.util.Comparator; import java.util.List; import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -76,7 +77,6 @@ class DefaultRepositoryRoleManagerTest { @Mock private RepositoryPermissionProvider permissionProvider; - @InjectMocks private DefaultRepositoryRoleManager manager; @BeforeEach @@ -103,6 +103,11 @@ class DefaultRepositoryRoleManagerTest { when(permissionProvider.availableRoles()).thenReturn(asList(CUSTOM_ROLE, SYSTEM_ROLE)); } + @BeforeEach + void initManager() { + manager = new DefaultRepositoryRoleManager(dao, permissionProvider, emptySet()); + } + @AfterEach void cleanupContext() { ThreadContext.unbindSubject(); diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java index 94870c645e..e0f40790bb 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java @@ -39,6 +39,7 @@ import sonia.scm.store.JAXBConfigurationStoreFactory; import java.nio.file.Path; import java.util.Optional; +import static java.util.Collections.emptySet; import static org.mockito.Mockito.when; import static sonia.scm.update.repository.MigrationStrategy.INLINE; @@ -53,7 +54,7 @@ class DefaultMigrationStrategyDAOTest { @BeforeEach void initStore(@TempDir Path tempDir) { when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); - storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null, null); + storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null, null, emptySet()); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java index 535d004c2b..202c1e8c6f 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java @@ -36,6 +36,9 @@ import sonia.scm.NotFoundException; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.user.xml.XmlUserDAO; +import java.util.Collections; + +import static java.util.Collections.emptySet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.anyString; @@ -64,7 +67,7 @@ public class DefaultUserManagerTest extends UserManagerTestBase { @Override public UserManager createManager() { - return new DefaultUserManager(passwordService, createXmlUserDAO()); + return new DefaultUserManager(passwordService, createXmlUserDAO(), emptySet()); } @Before @@ -78,7 +81,7 @@ public class DefaultUserManagerTest extends UserManagerTestBase { when(passwordService.encryptPassword(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); - userManager = new DefaultUserManager(passwordService, userDAO); + userManager = new DefaultUserManager(passwordService, userDAO, emptySet()); } @Test(expected = InvalidPasswordException.class) @@ -161,6 +164,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase { } private XmlUserDAO createXmlUserDAO() { - return new XmlUserDAO(new JAXBConfigurationStoreFactory(contextProvider, locationResolver, null)); + return new XmlUserDAO(new JAXBConfigurationStoreFactory(contextProvider, locationResolver, null, emptySet())); } }