/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geogig.geoserver.security;
import static java.lang.String.format;
import java.io.File;
import java.net.URI;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
import org.geogig.geoserver.config.LogStore;
import org.locationtech.geogig.model.ObjectId;
import org.locationtech.geogig.model.Ref;
import org.locationtech.geogig.porcelain.CloneOp;
import org.locationtech.geogig.porcelain.FetchOp;
import org.locationtech.geogig.porcelain.PullOp;
import org.locationtech.geogig.porcelain.PullResult;
import org.locationtech.geogig.porcelain.PushOp;
import org.locationtech.geogig.porcelain.RemoteAddOp;
import org.locationtech.geogig.porcelain.RemoteRemoveOp;
import org.locationtech.geogig.porcelain.TransferSummary;
import org.locationtech.geogig.porcelain.TransferSummary.ChangedRef;
import org.locationtech.geogig.porcelain.TransferSummary.ChangedRef.ChangeTypes;
import org.locationtech.geogig.repository.AbstractGeoGigOp;
import org.locationtech.geogig.repository.Context;
import org.locationtech.geogig.repository.Repository;
import org.springframework.beans.factory.InitializingBean;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
public class SecurityLogger implements InitializingBean {
private static final Map<Class<? extends AbstractGeoGigOp<?>>, MessageBuilder<?>> WATCHED_COMMANDS;
static {
Builder<Class<? extends AbstractGeoGigOp<?>>, MessageBuilder<?>> builder = ImmutableMap
.builder();
builder.put(RemoteAddOp.class, new RemoteAddMessageBuilder());
builder.put(RemoteRemoveOp.class, new RemoteRemoveMessageBuilder());
builder.put(PullOp.class, new PullOpMessageBuilder());
builder.put(PushOp.class, new PushOpMessageBuilder());
builder.put(FetchOp.class, new FetchOpMessageBuilder());
builder.put(CloneOp.class, new CloneOpMessageBuilder());
WATCHED_COMMANDS = builder.build();
}
private LogStore logStore;
private static SecurityLogger INSTANCE;
public SecurityLogger(LogStore logStore) {
this.logStore = logStore;
}
@Override
public void afterPropertiesSet() throws Exception {
INSTANCE = this;
}
public static boolean interestedIn(Class<? extends AbstractGeoGigOp<?>> clazz) {
return WATCHED_COMMANDS.containsKey(clazz);
}
public static void logPre(AbstractGeoGigOp<?> command) {
if (INSTANCE == null) {
return;// not yet initialized
}
INSTANCE.pre(command);
}
public static void logPost(AbstractGeoGigOp<?> command, @Nullable Object retVal,
@Nullable RuntimeException exception) {
if (INSTANCE == null) {
return;// not yet initialized
}
if (exception == null) {
INSTANCE.post(command, retVal);
} else {
INSTANCE.error(command, exception);
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void error(AbstractGeoGigOp<?> command, RuntimeException exception) {
MessageBuilder builder = builderFor(command);
String repoUrl = repoUrl(command);
logStore.error(repoUrl, builder.buildError(command, exception), exception);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void post(AbstractGeoGigOp<?> command, Object commandResult) {
MessageBuilder builder = builderFor(command);
String repoUrl = repoUrl(command);
logStore.info(repoUrl, builder.buildPost(command, commandResult));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void pre(AbstractGeoGigOp<?> command) {
MessageBuilder builder = builderFor(command);
String repoUrl = repoUrl(command);
logStore.debug(repoUrl, builder.buildPre(command));
}
private MessageBuilder<?> builderFor(AbstractGeoGigOp<?> command) {
MessageBuilder<?> builder = WATCHED_COMMANDS.get(command.getClass());
Preconditions.checkNotNull(builder);
return builder;
}
@Nullable
private String repoUrl(AbstractGeoGigOp<?> command) {
Context context = command.context();
if (context == null) {
return null;
}
Repository repository = context.repository();
if (repository == null) {
return null;
}
URI location = repository.getLocation();
if (location == null) {
return null;
}
String uri = location.toString();
if ("file".equals(location.getScheme())) {
try {
File f = new File(location);
if (f.getName().equals(".geogig")) {
f = f.getParentFile();
uri = f.toURI().toString();
}
} catch (Exception e) {
uri = location.toString();
}
}
return uri;
}
private static abstract class MessageBuilder<T extends AbstractGeoGigOp<?>> {
CharSequence buildPost(T command, Object commandResult) {
return format("%s success. Parameters: %s", friendlyName(), params(command));
}
CharSequence buildPre(T c) {
return format("%s: Parameters: %s", friendlyName(), params(c));
}
CharSequence buildError(T command, RuntimeException exception) {
return format("%s failed. Parameters: %s. Error message: %s", friendlyName(),
params(command), exception.getMessage());
}
abstract String friendlyName();
abstract String params(T command);
}
private static class RemoteAddMessageBuilder extends MessageBuilder<RemoteAddOp> {
@Override
String friendlyName() {
return "Remote add";
}
@Override
String params(RemoteAddOp c) {
return format("name='%s', url='%s'", c.getName(), c.getURL());
}
}
private static class RemoteRemoveMessageBuilder extends MessageBuilder<RemoteRemoveOp> {
@Override
String friendlyName() {
return "Remote remove";
}
@Override
String params(RemoteRemoveOp c) {
return format("name='%s'", c.getName());
}
}
private static class PullOpMessageBuilder extends MessageBuilder<PullOp> {
@Override
String friendlyName() {
return "Pull";
}
@Override
CharSequence buildPost(PullOp command, Object commandResult) {
PullResult pr = (PullResult) commandResult;
TransferSummary fr = pr.getFetchResult();
StringBuilder sb = formatFetchResult(fr);
return format("%s success. Parameters: %s. Changes: %s", friendlyName(),
params(command), sb);
}
@Override
String params(PullOp c) {
return format("remote=%s, refSpecs=%s, depth=%s, author=%s, author email=%s",
c.getRemoteName(), c.getRefSpecs(), c.getDepth(), c.getAuthor(),
c.getAuthorEmail());
}
}
private static class PushOpMessageBuilder extends MessageBuilder<PushOp> {
@Override
String friendlyName() {
return "Push";
}
@Override
String params(PushOp c) {
return format("remote=%s, refSpecs=%s", c.getRemoteName(), c.getRefSpecs());
}
}
private static class FetchOpMessageBuilder extends MessageBuilder<FetchOp> {
@Override
String friendlyName() {
return "Fetch";
}
@Override
CharSequence buildPost(FetchOp command, Object commandResult) {
TransferSummary fr = (TransferSummary) commandResult;
StringBuilder sb = formatFetchResult(fr);
return format("%s success. Parameters: %s. Changes: %s", friendlyName(),
params(command), sb);
}
@Override
String params(FetchOp c) {
return format("remotes=%s, all=%s, full depth=%s, depth=%s, prune=%s",
c.getRemoteNames(), c.isAll(), c.isFullDepth(), c.getDepth(), c.isPrune());
}
}
private static class CloneOpMessageBuilder extends MessageBuilder<CloneOp> {
@Override
String friendlyName() {
return "Clone";
}
@Override
String params(CloneOp c) {
return format("url=%s, branch=%s, depth=%s", c.getRepositoryURL().orNull(),
c.getBranch().orNull(), c.getDepth().orNull());
}
}
private static final StringBuilder formatFetchResult(TransferSummary fr) {
Map<String, Collection<ChangedRef>> refs = fr.getChangedRefs();
StringBuilder sb = new StringBuilder();
if (refs.isEmpty()) {
sb.append("already up to date");
} else {
for (Iterator<Entry<String, Collection<ChangedRef>>> it = refs.entrySet().iterator(); it
.hasNext();) {
Entry<String, Collection<ChangedRef>> entry = it.next();
String remoteUrl = entry.getKey();
Collection<ChangedRef> changedRefs = entry.getValue();
sb.append(" From ").append(remoteUrl).append(": [");
print(changedRefs, sb);
sb.append("]");
}
}
return sb;
}
private static String toString(ObjectId objectId) {
return objectId.toString().substring(0, 8);
}
private static void print(Collection<ChangedRef> changedRefs, StringBuilder sb) {
for (Iterator<ChangedRef> it = changedRefs.iterator(); it.hasNext();) {
ChangedRef ref = it.next();
Ref oldRef = ref.getOldRef();
Ref newRef = ref.getNewRef();
if (ref.getType() == ChangeTypes.CHANGED_REF) {
sb.append(oldRef.getName()).append(" ");
sb.append(toString(oldRef.getObjectId()));
sb.append(" -> ");
sb.append(toString(newRef.getObjectId()));
} else if (ref.getType() == ChangeTypes.ADDED_REF) {
String reftype = (newRef.getName().startsWith(Ref.TAGS_PREFIX)) ? "tag" : "branch";
sb.append("* [new ").append(reftype).append("] ").append(newRef.getName())
.append(" -> ").append(toString(newRef.getObjectId()));
} else if (ref.getType() == ChangeTypes.REMOVED_REF) {
sb.append("x [deleted] ").append(oldRef.getName());
} else {
sb.append("[deepened]" + newRef.getName());
}
if (it.hasNext()) {
sb.append(", ");
}
}
}
}