From 9bda1b2c1951e471d97f25658927f452f5e33489 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 22 Sep 2022 10:41:02 -0700 Subject: [PATCH] GUACAMOLE-1224: Add a default, global event listener providing logging. --- .../guacamole/event/AffectedObject.java | 124 +++++++++++++++++ .../guacamole/event/EventLoggingListener.java | 130 ++++++++++++++++++ .../org/apache/guacamole/event/Failure.java | 56 ++++++++ .../guacamole/event/LoggableDetail.java | 39 ++++++ .../guacamole/event/RequestingUser.java | 59 ++++++++ .../guacamole/extension/ExtensionModule.java | 4 +- 6 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/Failure.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java diff --git a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java new file mode 100644 index 000000000..572a9693a --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.event; + +import org.apache.guacamole.net.auth.Nameable; +import org.apache.guacamole.net.event.DirectoryEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Loggable representation of the object affected by an operation. + */ +public class AffectedObject implements LoggableDetail { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(AffectedObject.class); + + /** + * The event representing the requested operation. + */ + private final DirectoryEvent event; + + /** + * Creates a new AffectedObject representing the object affected by the + * operation described by the given event. + * + * @param event + * The event representing the operation. + */ + public AffectedObject(DirectoryEvent event) { + this.event = event; + } + + @Override + public String toString() { + + Object object = event.getObject(); + String identifier = event.getObjectIdentifier(); + + String objectType; + String name = null; // Not all objects have names + + // Obtain name of object (if applicable and available) + if (object instanceof Nameable) { + try { + name = ((Nameable) object).getName(); + } + catch (RuntimeException | Error e) { + logger.debug("Name of object \"{}\" could not be retrieved.", identifier, e); + } + } + + // Determine type of object + switch (event.getDirectoryType()) { + + // Active connections + case ACTIVE_CONNECTION: + objectType = "active connection"; + break; + + // Connections + case CONNECTION: + objectType = "connection"; + break; + + // Connection groups + case CONNECTION_GROUP: + objectType = "connection group"; + break; + + // Sharing profiles + case SHARING_PROFILE: + objectType = "sharing profile"; + break; + + // Users + case USER: + objectType = "user"; + break; + + // User groups + case USER_GROUP: + objectType = "user group"; + break; + + // Unknown + default: + objectType = (object != null) ? object.getClass().toString() : "an unknown object"; + + } + + // Describe at least the type of the object and its identifier, + // including the name of the object, as well, if available + if (identifier != null) { + if (name != null) + return objectType + " \"" + identifier + "\" (currently named \"" + name + "\")"; + else + return objectType + " \"" + identifier + "\""; + } + else + return objectType; + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java new file mode 100644 index 000000000..def5c19d9 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.event; + +import javax.annotation.Nonnull; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.net.event.DirectoryEvent; +import org.apache.guacamole.net.event.DirectoryFailureEvent; +import org.apache.guacamole.net.event.DirectorySuccessEvent; +import org.apache.guacamole.net.event.listener.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Listener that records each event that occurs in the logs, such as changes + * made to objects via the REST API. + */ +public class EventLoggingListener implements Listener { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(EventLoggingListener.class); + + /** + * Logs that an operation was performed on an object within a Directory + * successfully. + * + * @param event + * The event describing the operation successfully performed on the + * object. + */ + private void logSuccess(DirectorySuccessEvent event) { + DirectoryEvent.Operation op = event.getOperation(); + switch (op) { + + case GET: + logger.debug("{} successfully accessed/retrieved {}", new RequestingUser(event), new AffectedObject(event)); + break; + + case ADD: + logger.info("{} successfully created {}", new RequestingUser(event), new AffectedObject(event)); + break; + + case UPDATE: + logger.info("{} successfully updated {}", new RequestingUser(event), new AffectedObject(event)); + break; + + case REMOVE: + logger.info("{} successfully deleted {}", new RequestingUser(event), new AffectedObject(event)); + break; + + default: + logger.warn("DirectoryEvent operation type has no corresponding log message implemented: {}", op); + logger.info("{} successfully performed an unknown action on {} {}", new RequestingUser(event), new AffectedObject(event)); + + } + } + + /** + * Logs that an operation failed to be performed on an object within a + * Directory. + * + * @param event + * The event describing the operation that failed. + */ + private void logFailure(DirectoryFailureEvent event) { + DirectoryEvent.Operation op = event.getOperation(); + switch (op) { + + case GET: + if (event.getFailure() instanceof GuacamoleResourceNotFoundException) + logger.debug("{} failed to access/retrieve {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + else + logger.info("{} failed to access/retrieve {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + case ADD: + logger.info("{} failed to create {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + case UPDATE: + logger.info("{} failed to update {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + case REMOVE: + logger.info("{} failed to delete {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + default: + logger.warn("DirectoryEvent operation type has no corresponding log message implemented: {}", op); + logger.info("{} failed to perform an unknown action on {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + + } + } + + @Override + public void handleEvent(@Nonnull Object event) throws GuacamoleException { + + if (event instanceof DirectorySuccessEvent) + logSuccess((DirectorySuccessEvent) event); + + else if (event instanceof DirectoryFailureEvent) + logFailure((DirectoryFailureEvent) event); + + else + logger.debug("Ignoring unknown/unimplemented event type: {}", + event.getClass()); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/Failure.java b/guacamole/src/main/java/org/apache/guacamole/event/Failure.java new file mode 100644 index 000000000..10b3c960b --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/Failure.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.event; + +import org.apache.guacamole.net.event.FailureEvent; + +/** + * Loggable representation of a failure that occurred. + */ +public class Failure implements LoggableDetail { + + /** + * The event representing the failure. + */ + private final FailureEvent event; + + /** + * Creates a new Failure representing the failure described by the given + * event. + * + * @param event + * The event representing the failure. + */ + public Failure(FailureEvent event) { + this.event = event; + } + + @Override + public String toString() { + + Throwable failure = event.getFailure(); + if (failure == null) + return "unknown error (no specific failure recorded)"; + + return failure.getMessage(); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java b/guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java new file mode 100644 index 000000000..909412e40 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.event; + +/** + * Provides a {@link #toString()} implementation that returns a human-readable + * string that is intended to be logged and which describes a particular detail + * of an event. + */ +public interface LoggableDetail { + + /** + * {@inheritDoc} + *

+ * A LoggableDetail implementation of toString() is required to return a + * string that is human-readable, describes a detail of a provided event, + * and that is intended to be logged. + */ + @Override + String toString(); + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java b/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java new file mode 100644 index 000000000..124aa8f80 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.event; + +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.event.DirectoryEvent; + +/** + * Loggable representation of the user that requested an operation. + */ +public class RequestingUser implements LoggableDetail { + + /** + * The event representing the requested operation. + */ + private final DirectoryEvent event; + + /** + * Creates a new RequestingUser that represents the user that requested the + * operation described by the given event. + * + * @param event + * The event representing the requested operation. + */ + public RequestingUser(DirectoryEvent event) { + this.event = event; + } + + @Override + public String toString() { + + AuthenticatedUser user = event.getAuthenticatedUser(); + String identifier = user.getIdentifier(); + + if (AuthenticatedUser.ANONYMOUS_IDENTIFIER.equals(identifier)) + return "Anonymous user (authenticated by \"" + user.getAuthenticationProvider().getIdentifier() + "\")"; + + return "User \"" + identifier + "\" (authenticated by \"" + user.getAuthenticationProvider().getIdentifier() + "\")"; + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java index 0fcff57dd..e5fbe2e57 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java @@ -35,6 +35,7 @@ import org.apache.guacamole.auth.file.FileAuthenticationProvider; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.event.EventLoggingListener; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.event.listener.Listener; import org.apache.guacamole.properties.StringSetProperty; @@ -628,8 +629,9 @@ public class ExtensionModule extends ServletModule { final Set toleratedAuthProviders = getToleratedAuthenticationProviders(); loadExtensions(javaScriptResources, cssResources, toleratedAuthProviders); - // Always bind default file-driven auth last + // Always bind default file-driven auth and event logging last bindAuthenticationProvider(FileAuthenticationProvider.class, toleratedAuthProviders); + bindListener(EventLoggingListener.class); // Dynamically generate app.js and app.css from extensions serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources)));