mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 03:25:56 +01:00
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.
This commit is contained in:
committed by
SCM-Manager
parent
e74225e168
commit
56265be9a2
2
gradle/changelog/audit_log.yaml
Normal file
2
gradle/changelog/audit_log.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Initial implementation of an audit log API
|
||||
38
scm-core/src/main/java/sonia/scm/auditlog/AuditEntry.java
Normal file
38
scm-core/src/main/java/sonia/scm/auditlog/AuditEntry.java
Normal file
@@ -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 {};
|
||||
}
|
||||
@@ -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<T> implements ConfigurationStore<T> {
|
||||
|
||||
private final Set<Auditor> auditors;
|
||||
private final RepositoryDAO repositoryDAO;
|
||||
private final ConfigurationStore<T> delegate;
|
||||
private final StoreDecoratorFactory.Context context;
|
||||
|
||||
public AuditLogConfigurationStoreDecorator(Set<Auditor> auditors, RepositoryDAO repositoryDAO, ConfigurationStore<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Auditor> auditors;
|
||||
private final RepositoryDAO repositoryDAO;
|
||||
|
||||
@Inject
|
||||
public AuditLogConfigurationStoreDecoratorFactory(Set<Auditor> auditor, RepositoryDAO repositoryDAO) {
|
||||
this.auditors = auditor;
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ConfigurationStore<T> createDecorator(ConfigurationStore<T> object, Context context) {
|
||||
return new AuditLogConfigurationStoreDecorator<>(auditors, repositoryDAO, object, context);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
32
scm-core/src/main/java/sonia/scm/auditlog/Auditor.java
Normal file
32
scm-core/src/main/java/sonia/scm/auditlog/Auditor.java
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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<T> {
|
||||
private final T object;
|
||||
private final T oldObject;
|
||||
private final String entity;
|
||||
private final Set<String> additionalLabels;
|
||||
|
||||
public EntryCreationContext(T object, T oldObject) {
|
||||
this(object, oldObject, "", emptySet());
|
||||
}
|
||||
|
||||
public EntryCreationContext(T object, T oldObject, Set<String> additionalLabels) {
|
||||
this(object, oldObject, "", additionalLabels);
|
||||
}
|
||||
|
||||
public EntryCreationContext(T object, T oldObject, String entity, Set<String> additionalLabels) {
|
||||
this.object = object;
|
||||
this.oldObject = oldObject;
|
||||
this.entity = entity;
|
||||
this.additionalLabels = additionalLabels;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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<String> members)
|
||||
{
|
||||
public Group(String type, String name, List<String> 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<String> getMembers()
|
||||
{
|
||||
public List<String> 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<String> members)
|
||||
{
|
||||
public void setMembers(List<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
@@ -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 {
|
||||
<T> ConfigurationStore<T> createDecorator(ConfigurationStore<T> object, Context context);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
<T> ConfigurationStore<T> createDecorator(ConfigurationStore<T> object, Context context);
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
class Context {
|
||||
private TypedStoreParameters<?> storeParameters;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ConfigurationStoreDecoratorFactory> 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<ConfigurationStoreDecoratorFactory> decoratorFactories) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.CONFIG, readOnlyChecker);
|
||||
this.decoratorFactories = decoratorFactories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> JAXBConfigurationStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||
public <T> ConfigurationStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||
TypedStoreContext<T> context = TypedStoreContext.of(storeParameters);
|
||||
return new JAXBConfigurationStore<>(
|
||||
|
||||
ConfigurationStore<T> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
@@ -25,8 +25,12 @@
|
||||
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<T extends ModelObject> {
|
||||
|
||||
private final GenericDAO<T> dao;
|
||||
private final Set<Auditor> auditors;
|
||||
|
||||
public ManagerDaoAdapter(GenericDAO<T> dao) {
|
||||
public ManagerDaoAdapter(GenericDAO<T> dao, Set<Auditor> auditors) {
|
||||
this.dao = dao;
|
||||
this.auditors = auditors;
|
||||
}
|
||||
|
||||
public void modify(T object, Function<T, PermissionCheck> permissionCheck, AroundHandler<T> beforeUpdate, AroundHandler<T> afterUpdate) {
|
||||
@@ -51,6 +57,8 @@ public class ManagerDaoAdapter<T extends ModelObject> {
|
||||
object.setLastModified(System.currentTimeMillis());
|
||||
object.setCreationDate(notModified.getCreationDate());
|
||||
|
||||
callAuditors(notModified, object);
|
||||
|
||||
dao.modify(object);
|
||||
|
||||
afterUpdate.handle(notModified);
|
||||
@@ -73,6 +81,7 @@ public class ManagerDaoAdapter<T extends ModelObject> {
|
||||
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<T extends ModelObject> {
|
||||
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<T extends ModelObject> {
|
||||
}
|
||||
}
|
||||
|
||||
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<T extends ModelObject> {
|
||||
void handle(T notModified);
|
||||
|
||||
@@ -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<Auditor> 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<Group> check = GroupPermissions.read();
|
||||
|
||||
return Util.createSubCollection(groupDAO.getAll(), comparator,
|
||||
new CollectionAppender<Group>()
|
||||
{
|
||||
@Override
|
||||
public void append(Collection<Group> collection, Group group)
|
||||
{
|
||||
(collection, group) -> {
|
||||
if (check.isPermitted(group)) {
|
||||
collection.add(group.clone());
|
||||
}
|
||||
}
|
||||
}, start, limit);
|
||||
}, start, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 <T> ConfigurationStore<T> createDecorator(ConfigurationStore<T> object, Context context) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ConfigurationStoreDecoratorFactory> storeDecoratorMultiBinder = Multibinder.newSetBinder(binder(), ConfigurationStoreDecoratorFactory.class);
|
||||
storeDecoratorMultiBinder.addBinding().to(AuditLogConfigurationStoreDecoratorFactory.class);
|
||||
|
||||
// bind repository provider
|
||||
ThrowingProviderBinder.create(binder())
|
||||
.bind(RepositoryProvider.class, Repository.class)
|
||||
|
||||
@@ -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<RepositoryHandler> handlerSet,
|
||||
Provider<NamespaceStrategy> namespaceStrategyProvider, RepositoryPostProcessor repositoryPostProcessor) {
|
||||
public DefaultRepositoryManager(SCMContextProvider contextProvider,
|
||||
KeyGenerator keyGenerator,
|
||||
RepositoryDAO repositoryDAO,
|
||||
Set<RepositoryHandler> handlerSet,
|
||||
Provider<NamespaceStrategy> namespaceStrategyProvider,
|
||||
RepositoryPostProcessor repositoryPostProcessor,
|
||||
Set<Auditor> 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
|
||||
|
||||
@@ -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<Auditor> 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<RepositoryRole> getAll(Comparator<RepositoryRole> 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<RepositoryRole> getAll(int start, int limit)
|
||||
{
|
||||
public Collection<RepositoryRole> getAll(int start, int limit) {
|
||||
return getAll(null, start, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getLastModified()
|
||||
{
|
||||
public Long getLastModified() {
|
||||
return repositoryRoleDAO.getLastModified();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Auditor> auditors)
|
||||
{
|
||||
this.passwordService = passwordService;
|
||||
this.userDAO = userDAO;
|
||||
this.managerDaoAdapter = new ManagerDaoAdapter<>(userDAO);
|
||||
this.managerDaoAdapter = new ManagerDaoAdapter<>(userDAO, auditors);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@@ -119,7 +119,8 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
repositoryDAO,
|
||||
handlerSet,
|
||||
Providers.of(namespaceStrategy),
|
||||
repositoryPostProcessor);
|
||||
repositoryPostProcessor,
|
||||
Collections.emptySet());
|
||||
|
||||
setUpTestRepositories();
|
||||
|
||||
|
||||
@@ -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<Repository> {
|
||||
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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user