GUACAMOLE-1364: Merge add support for overriding extension priority without renaming files.

This commit is contained in:
Virtually Nick
2021-06-15 17:16:11 -04:00
committed by GitHub
11 changed files with 339 additions and 77 deletions

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.3.0",
"name" : "CAS Authentication Extension",
"namespace" : "guac-cas",
"namespace" : "cas",
"authProviders" : [
"org.apache.guacamole.auth.cas.CASAuthenticationProvider"

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.2.0",
"name" : "HTTP Header Authentication Extension",
"namespace" : "guac-header",
"namespace" : "header",
"authProviders" : [
"org.apache.guacamole.auth.header.HTTPHeaderAuthenticationProvider"

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.3.0",
"name" : "MySQL Authentication",
"namespace" : "guac-mysql",
"namespace" : "mysql",
"authProviders" : [
"org.apache.guacamole.auth.mysql.MySQLAuthenticationProvider",

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.3.0",
"name" : "PostgreSQL Authentication",
"namespace" : "guac-postgresql",
"namespace" : "postgresql",
"authProviders" : [
"org.apache.guacamole.auth.postgresql.PostgreSQLAuthenticationProvider",

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.3.0",
"name" : "SQLServer Authentication",
"namespace" : "guac-sqlserver",
"namespace" : "sqlserver",
"authProviders" : [
"org.apache.guacamole.auth.sqlserver.SQLServerAuthenticationProvider",

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.3.0",
"name" : "Encrypted JSON Authentication",
"namespace" : "guac-json",
"namespace" : "json",
"authProviders" : [
"org.apache.guacamole.auth.json.JSONAuthenticationProvider"

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.3.0",
"name" : "LDAP Authentication",
"namespace" : "guac-ldap",
"namespace" : "ldap",
"authProviders" : [
"org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider"

View File

@@ -3,7 +3,7 @@
"guacamoleVersion" : "1.3.0",
"name" : "OpenID Authentication Extension",
"namespace" : "guac-openid",
"namespace" : "openid",
"authProviders" : [
"org.apache.guacamole.auth.openid.OpenIDAuthenticationProvider"

View File

@@ -26,7 +26,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
@@ -55,6 +55,11 @@ public class Extension {
*/
private static final String MANIFEST_NAME = "guac-manifest.json";
/**
* The extension .jar file.
*/
private final File file;
/**
* The parsed manifest file of this extension, describing the location of
* resources within the extension.
@@ -144,7 +149,7 @@ public class Extension {
return Collections.<String, Resource>emptyMap();
// Add classpath resource for each path provided
Map<String, Resource> resources = new HashMap<String, Resource>(paths.size());
Map<String, Resource> resources = new LinkedHashMap<>(paths.size());
for (String path : paths)
resources.put(path, new ClassPathResource(classLoader, mimetype, path));
@@ -173,7 +178,7 @@ public class Extension {
return Collections.<String, Resource>emptyMap();
// Add classpath resource for each path/mimetype pair provided
Map<String, Resource> resources = new HashMap<String, Resource>(resourceTypes.size());
Map<String, Resource> resources = new LinkedHashMap<>(resourceTypes.size());
for (Map.Entry<String, String> resource : resourceTypes.entrySet()) {
// Get path and mimetype from entry
@@ -357,6 +362,9 @@ public class Extension {
*/
public Extension(final ClassLoader parent, final File file) throws GuacamoleException {
// Associate extension abstraction with original file
this.file = file;
try {
// Open extension
@@ -427,6 +435,16 @@ public class Extension {
largeIcon = null;
}
/**
* Returns the .jar file containing this Guacamole extension.
*
* @return
* The extension .jar file.
*/
public File getFile() {
return file;
}
/**
* Returns the version of the Guacamole web application for which this
* extension was built.

View File

@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -40,7 +41,6 @@ import org.apache.guacamole.properties.StringSetProperty;
import org.apache.guacamole.resource.Resource;
import org.apache.guacamole.resource.ResourceServlet;
import org.apache.guacamole.resource.SequenceResource;
import org.apache.guacamole.resource.WebApplicationResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -105,6 +105,22 @@ public class ExtensionModule extends ServletModule {
};
/**
* A comma-separated list of the namespaces of all extensions that should
* be loaded in a specific order. The special value "*" can be used in
* lieu of a namespace to represent all extensions that are not listed. All
* extensions explicitly listed will be sorted in the order given, while
* all extensions not explicitly listed will be sorted by their filenames.
*/
public static final ExtensionOrderProperty EXTENSION_PRIORITY = new ExtensionOrderProperty() {
@Override
public String getName() {
return "extension-priority";
}
};
/**
* The Guacamole server environment.
*/
@@ -394,9 +410,101 @@ public class ExtensionModule extends ServletModule {
}
/**
* Returns a comparator that sorts extensions by their desired load order,
* as dictated by the "extension-priority" property and their filenames.
*
* @return
* A comparator that sorts extensions by their desired load order.
*/
private Comparator<Extension> getExtensionLoadOrder() {
// Parse desired sort order of extensions
try {
return environment.getProperty(EXTENSION_PRIORITY, ExtensionOrderProperty.DEFAULT_COMPARATOR);
}
// Sort by filename if the desired order cannot be read
catch (GuacamoleException e) {
logger.warn("The list of extensions specified via the \"{}\" property could not be parsed: {}", EXTENSION_PRIORITY.getName(), e.getMessage());
logger.debug("Unable to parse \"{}\" property.", EXTENSION_PRIORITY.getName(), e);
return ExtensionOrderProperty.DEFAULT_COMPARATOR;
}
}
/**
* Returns a list of all installed extensions in the order they should be
* loaded. Extension load order is dictated by the "extension-priority"
* property and by extension filename. Each extension within
* GUACAMOLE_HOME/extensions is read and validated, but not fully loaded.
* It is the responsibility of the caller to continue the load process with
* the extensions in the returned list.
*
* @return
* A list of all installed extensions, ordered by load priority.
*/
private List<Extension> getExtensions() {
// Retrieve and validate extensions directory
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
if (!extensionsDir.isDirectory())
return Collections.emptyList();
// Retrieve list of all extension files within extensions directory
File[] extensionFiles = extensionsDir.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(EXTENSION_SUFFIX);
}
});
// Verify contents are accessible
if (extensionFiles == null) {
logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read.");
return Collections.emptyList();
}
// Read (but do not fully load) each extension within the extension
// directory
List<Extension> extensions = new ArrayList<>(extensionFiles.length);
for (File extensionFile : extensionFiles) {
logger.debug("Reading extension: \"{}\"", extensionFile.getName());
try {
// Load extension from file
Extension extension = new Extension(getParentClassLoader(), extensionFile);
// Validate Guacamole version of extension
if (!isCompatible(extension.getGuacamoleVersion())) {
logger.debug("Declared Guacamole version \"{}\" of extension \"{}\" is not compatible with this version of Guacamole.",
extension.getGuacamoleVersion(), extensionFile.getName());
throw new GuacamoleServerException("Extension \"" + extension.getName() + "\" is not "
+ "compatible with this version of Guacamole.");
}
extensions.add(extension);
}
catch (GuacamoleException e) {
logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage());
logger.debug("Unable to load extension.", e);
}
}
extensions.sort(getExtensionLoadOrder());
return extensions;
}
/**
* Loads all extensions within the GUACAMOLE_HOME/extensions directory, if
* any, adding their static resource to the given resoure collections.
* any, adding their static resource to the given resource collections.
*
* @param javaScriptResources
* A modifiable collection of static JavaScript resources which may
@@ -420,47 +528,26 @@ public class ExtensionModule extends ServletModule {
Collection<Resource> cssResources,
Set<String> toleratedAuthProviders) {
// Retrieve and validate extensions directory
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
if (!extensionsDir.isDirectory())
return;
// Advise of current extension load order and how the order may be
// changed
List<Extension> extensions = getExtensions();
if (extensions.size() > 1) {
logger.info("Multiple extensions are installed and will be "
+ "loaded in order of decreasing priority:");
// Retrieve list of all extension files within extensions directory
File[] extensionFiles = extensionsDir.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(EXTENSION_SUFFIX);
for (Extension extension : extensions) {
logger.info(" - [{}] \"{}\" ({})", extension.getNamespace(),
extension.getName(), extension.getFile());
}
});
// Verify contents are accessible
if (extensionFiles == null) {
logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read.");
return;
logger.info("To change this order, set the \"{}\" property or "
+ "rename the extension files. The default priority of "
+ "extensions is dictated by the sort order of their "
+ "filenames.", EXTENSION_PRIORITY.getName());
}
// Sort files lexicographically
Arrays.sort(extensionFiles);
// Load each extension within the extension directory
for (File extensionFile : extensionFiles) {
logger.debug("Loading extension: \"{}\"", extensionFile.getName());
try {
// Load extension from file
Extension extension = new Extension(getParentClassLoader(), extensionFile);
// Validate Guacamole version of extension
if (!isCompatible(extension.getGuacamoleVersion())) {
logger.debug("Declared Guacamole version \"{}\" of extension \"{}\" is not compatible with this version of Guacamole.",
extension.getGuacamoleVersion(), extensionFile.getName());
throw new GuacamoleServerException("Extension \"" + extension.getName() + "\" is not "
+ "compatible with this version of Guacamole.");
}
// Load all extensions
for (Extension extension : extensions) {
// Add any JavaScript / CSS resources
javaScriptResources.addAll(extension.getJavaScriptResources().values());
@@ -491,13 +578,7 @@ public class ExtensionModule extends ServletModule {
serve("/images/logo-144.png").with(new ResourceServlet(extension.getLargeIcon()));
// Log successful loading of extension by name
logger.info("Extension \"{}\" loaded.", extension.getName());
}
catch (GuacamoleException e) {
logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage());
logger.debug("Unable to load extension.", e);
}
logger.info("Extension \"{}\" ({}) loaded.", extension.getName(), extension.getNamespace());
}

View File

@@ -0,0 +1,163 @@
/*
* 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.extension;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.properties.GuacamoleProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A GuacamoleProperty that defines the order of Guacamole extensions. The
* property value is a comma-separated list of extension namespaces, with "*"
* used to represent all extensions that aren't listed. For example, a value
* like "saml, *, ldap" would order SAML support first and LDAP support last,
* with all other extensions loaded between the two in filename order. For
* values without "*", all other extensions are implicitly after all extensions
* that are explicitly listed.
*/
public abstract class ExtensionOrderProperty implements GuacamoleProperty<Comparator<Extension>> {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ExtensionOrderProperty.class);
/**
* A pattern which matches against the delimiters between values. This is
* currently simply a comma and any following whitespace. Parts of the
* input string which match this pattern will not be included in the parsed
* result.
*/
private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*");
/**
* Static comparator instance that sorts extensions by their filenames
* alone.
*/
public static final Comparator<Extension> DEFAULT_COMPARATOR = new ExtensionComparator();
/**
* Comparator that sorts extensions in order of priority, as dictated by a
* list of the extensions that should be ordered first or last. All
* extensions not explicitly listed will instead be sorted by filename.
*/
private static class ExtensionComparator implements Comparator<Extension> {
/**
* The string value representing the set of all extensions not
* explicitly listed.
*/
private final String OTHER_EXTENSIONS = "*";
/**
* The relative priorities of all extensions. Any extension not listed
* within this map should be sorted with the priority value stored in
* {@link #defaultPriority}.
*/
private final Map<String, Integer> extensionPriority;
/**
* The relative priority that should be used for all extensions not
* explicitly listed within {@link #extensionPriority}.
*/
private final int defaultPriority;
/**
* Creates a new ExtensionComparator that sorts all extensions by their
* filenames only.
*/
public ExtensionComparator() {
defaultPriority = 0;
extensionPriority = Collections.emptyMap();
}
/**
* Creates a new ExtensionComparator that ensures each of the given
* extensions are sorted in the relative order listed, with any
* extensions not explicitly listed sorted by filename.
*
* @param name
* The name of the property defining the provided list of
* extensions.
*
* @param extensions
* The namespaces of the extensions in the order they should be
* sorted, with the special value "*" functioning as a
* placeholder for all extensions that are not explicitly listed.
*/
public ExtensionComparator(String name, String... extensions) {
extensionPriority = new HashMap<>(extensions.length);
for (int priority = 0; priority < extensions.length; priority++) {
String extension = extensions[priority];
if (extensionPriority.putIfAbsent(extension, priority) != null)
logger.warn("The value \"{}\" was specified multiple "
+ "times for property \"{}\". Only the first "
+ "occurrence of this value will have any effect.",
extension, name);
}
Integer otherExtensionPriority = extensionPriority.remove(OTHER_EXTENSIONS);
if (otherExtensionPriority != null)
defaultPriority = otherExtensionPriority;
else
defaultPriority = extensions.length;
}
@Override
public int compare(Extension extA, Extension extB) {
int priorityA = extensionPriority.getOrDefault(extA.getNamespace(), defaultPriority);
int priorityB = extensionPriority.getOrDefault(extB.getNamespace(), defaultPriority);
// Sort by explicit priority first
if (priorityA != priorityB)
return priorityA - priorityB;
// Sort all extensions without explicit priorities by their
// filenames (no extensions will have the same priority except
// those that aren't explicitly listed)
return extA.getFile().compareTo(extB.getFile());
}
}
@Override
public Comparator<Extension> parseValue(String value) throws GuacamoleException {
// If no property provided, return null.
if (value == null)
return null;
// Split string into a set of individual values
return new ExtensionComparator(getName(), DELIMITER_PATTERN.split(value));
}
}