diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java index 93cc7f7a3..9158afb85 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java @@ -22,6 +22,7 @@ package org.apache.guacamole.auth.jdbc; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.auth.jdbc.security.PasswordPolicy; +import org.apache.ibatis.session.SqlSession; /** * A JDBC-specific implementation of Environment that defines generic properties @@ -143,9 +144,12 @@ public abstract class JDBCEnvironment extends LocalEnvironment { * not supported, queries that are intended to be recursive may need to be * invoked multiple times to retrieve the same data. * + * @param session + * The SqlSession provided by MyBatis for the current transaction. + * * @return * true if the database supports recursive queries, false otherwise. */ - public abstract boolean isRecursiveQuerySupported(); + public abstract boolean isRecursiveQuerySupported(SqlSession session); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityMapper.java index 53b029091..dbe7cb4d0 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityMapper.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityMapper.java @@ -60,12 +60,21 @@ public interface EntityMapper { * The identifiers of any known effective groups that should be taken * into account, such as those defined externally to the database. * + * @param recursive + * Whether the query should leverage database engine features to return + * absolutely all effective groups, including those inherited through + * group membership. If false, this query will return only one level of + * depth and may need to be executed multiple times. If it is known + * that the database engine in question will always support (or always + * not support) recursive queries, this parameter may be ignored. + * * @return * The set of identifiers of all groups that the given entity is a * member of, including those where membership is inherited through * membership in other groups. */ Set selectEffectiveGroupIdentifiers(@Param("entity") EntityModel entity, - @Param("effectiveGroups") Collection effectiveGroups); + @Param("effectiveGroups") Collection effectiveGroups, + @Param("recursive") boolean recursive); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityService.java index 1e40bb0ae..cc2a9aaf9 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/EntityService.java @@ -23,6 +23,8 @@ import com.google.inject.Inject; import java.util.Collection; import java.util.Set; import org.apache.guacamole.auth.jdbc.JDBCEnvironment; +import org.apache.ibatis.session.SqlSession; +import org.mybatis.guice.transactional.Transactional; /** * Service which provides convenience methods for creating, retrieving, and @@ -42,6 +44,12 @@ public class EntityService { @Inject private EntityMapper entityMapper; + /** + * The current SQL session used by MyBatis. + */ + @Inject + private SqlSession sqlSession; + /** * Returns the set of all group identifiers of which the given entity is a * member, taking into account the given collection of known group @@ -64,20 +72,22 @@ public class EntityService { * member of, including those where membership is inherited through * membership in other groups. */ + @Transactional public Set retrieveEffectiveGroups(ModeledPermissions entity, Collection effectiveGroups) { // Retrieve the effective user groups of the given entity, recursively if possible - Set identifiers = entityMapper.selectEffectiveGroupIdentifiers(entity.getModel(), effectiveGroups); + boolean recursive = environment.isRecursiveQuerySupported(sqlSession); + Set identifiers = entityMapper.selectEffectiveGroupIdentifiers(entity.getModel(), effectiveGroups, recursive); // If the set of user groups retrieved was not produced recursively, // manually repeat the query to expand the set until all effective // groups have been found - if (!environment.isRecursiveQuerySupported() && !identifiers.isEmpty()) { + if (!recursive && !identifiers.isEmpty()) { Set previousIdentifiers; do { previousIdentifiers = identifiers; - identifiers = entityMapper.selectEffectiveGroupIdentifiers(entity.getModel(), previousIdentifiers); + identifiers = entityMapper.selectEffectiveGroupIdentifiers(entity.getModel(), previousIdentifiers, false); } while (identifiers.size() > previousIdentifiers.size()); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLEnvironment.java index 062d6dfa4..7a9315197 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLEnvironment.java @@ -19,11 +19,16 @@ package org.apache.guacamole.auth.mysql; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.guacamole.auth.jdbc.security.PasswordPolicy; +import org.apache.ibatis.exceptions.PersistenceException; +import org.apache.ibatis.session.SqlSession; /** * A MySQL-specific implementation of JDBCEnvironment provides database @@ -35,7 +40,17 @@ public class MySQLEnvironment extends JDBCEnvironment { * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(MySQLEnvironment.class); - + + /** + * The earliest version of MariaDB that supported recursive CTEs. + */ + private static final MySQLVersion MARIADB_SUPPORTS_CTE = new MySQLVersion(10, 2, 2, true); + + /** + * The earliest version of MySQL that supported recursive CTEs. + */ + private static final MySQLVersion MYSQL_SUPPORTS_CTE = new MySQLVersion(8, 0, 1, false); + /** * The default host to connect to, if MYSQL_HOSTNAME is not specified. */ @@ -227,8 +242,39 @@ public class MySQLEnvironment extends JDBCEnvironment { } @Override - public boolean isRecursiveQuerySupported() { - return false; // Only very recent versions of MySQL / MariaDB support recursive queries through CTEs + public boolean isRecursiveQuerySupported(SqlSession session) { + + // Retrieve database version string from JDBC connection + String versionString; + try { + Connection connection = session.getConnection(); + DatabaseMetaData metaData = connection.getMetaData(); + versionString = metaData.getDatabaseProductVersion(); + } + catch (SQLException e) { + throw new PersistenceException("Cannot determine whether " + + "MySQL / MariaDB supports recursive queries.", e); + } + + try { + + // Parse MySQL / MariaDB version from version string + MySQLVersion version = new MySQLVersion(versionString); + logger.debug("Database recognized as {}.", version); + + // Recursive queries are supported for MariaDB 10.2.2+ and + // MySQL 8.0.1+ + return version.isAtLeast(MARIADB_SUPPORTS_CTE) + || version.isAtLeast(MYSQL_SUPPORTS_CTE); + + } + catch (IllegalArgumentException e) { + logger.debug("Unrecognized MySQL / MariaDB version string: " + + "\"{}\". Assuming database engine does not support " + + "recursive queries.", session); + return false; + } + } } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLVersion.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLVersion.java new file mode 100644 index 000000000..577506ef0 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLVersion.java @@ -0,0 +1,153 @@ +/* + * 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.auth.mysql; + +import com.google.common.collect.ComparisonChain; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The specific version of a MySQL or MariaDB server. + */ +public class MySQLVersion { + + /** + * Pattern which matches the version string returned by a MariaDB server, + * extracting the major, minor, and patch numbers. + */ + private final Pattern MARIADB_VERSION = Pattern.compile("^.*-([0-9]+)\\.([0-9]+)\\.([0-9]+)-MariaDB$"); + + /** + * Pattern which matches the version string returned by a non-MariaDB + * server (including MySQL and Aurora), extracting the major, minor, and + * patch numbers. All non-MariaDB servers use normal MySQL version numbers. + */ + private final Pattern MYSQL_VERSION = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+).*$"); + + /** + * Whether the associated server is a MariaDB server. All non-MariaDB + * servers use normal MySQL version numbers and are comparable against each + * other. + */ + private final boolean isMariaDB; + + /** + * The major component of the MAJOR.MINOR.PATCH version number. + */ + private final int major; + + /** + * The minor component of the MAJOR.MINOR.PATCH version number. + */ + private final int minor; + + /** + * The patch component of the MAJOR.MINOR.PATCH version number. + */ + private final int patch; + + /** + * Creates a new MySQLVersion having the specified major, minor, and patch + * components. + * + * @param major + * The major component of the MAJOR.MINOR.PATCH version number of the + * MariaDB / MySQL server. + * + * @param minor + * The minor component of the MAJOR.MINOR.PATCH version number of the + * MariaDB / MySQL server. + * + * @param patch + * The patch component of the MAJOR.MINOR.PATCH version number of the + * MariaDB / MySQL server. + * + * @param isMariaDB + * Whether the associated server is a MariaDB server. + */ + public MySQLVersion(int major, int minor, int patch, boolean isMariaDB) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.isMariaDB = isMariaDB; + } + + public MySQLVersion(String version) throws IllegalArgumentException { + + // Extract MariaDB version number if version string appears to be + // a MariaDB version string + Matcher mariadb = MARIADB_VERSION.matcher(version); + if (mariadb.matches()) { + this.major = Integer.parseInt(mariadb.group(1)); + this.minor = Integer.parseInt(mariadb.group(2)); + this.patch = Integer.parseInt(mariadb.group(3)); + this.isMariaDB = true; + return; + } + + // If not MariaDB, assume version string is a MySQL version string + // and attempt to extract the version number + Matcher mysql = MYSQL_VERSION.matcher(version); + if (mysql.matches()) { + this.major = Integer.parseInt(mysql.group(1)); + this.minor = Integer.parseInt(mysql.group(2)); + this.patch = Integer.parseInt(mysql.group(3)); + this.isMariaDB = false; + return; + } + + throw new IllegalArgumentException("Unrecognized MySQL / MariaDB version string."); + + } + + /** + * Returns whether this version is at least as recent as the given version. + * + * @param version + * The version to compare against. + * + * @return + * true if the versions are associated with the same database server + * type (MariaDB vs. MySQL) and this version is at least as recent as + * the given version, false otherwise. + */ + public boolean isAtLeast(MySQLVersion version) { + + // If the databases use different version numbering schemes, the + // version numbers are not comparable + if (isMariaDB != version.isMariaDB) + return false; + + // Compare major, minor, and patch number in order of precedence + return ComparisonChain.start() + .compare(major, version.major) + .compare(minor, version.minor) + .compare(patch, version.patch) + .result() >= 0; + + } + + @Override + public String toString() { + return String.format("%s %d.%d.%d", isMariaDB ? "MariaDB" : "MySQL", + major, minor, patch); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml index eb7a7714a..21efb9954 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml @@ -65,43 +65,76 @@ diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLEnvironment.java index d5d259e6f..4ac99e8d1 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLEnvironment.java @@ -24,6 +24,7 @@ import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.guacamole.auth.jdbc.security.PasswordPolicy; +import org.apache.ibatis.session.SqlSession; /** * A PostgreSQL-specific implementation of JDBCEnvironment provides database @@ -244,7 +245,7 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { } @Override - public boolean isRecursiveQuerySupported() { + public boolean isRecursiveQuerySupported(SqlSession session) { return true; // All versions of PostgreSQL support recursive queries through CTEs } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/SQLServerEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/SQLServerEnvironment.java index 03f2cf865..db068b9b8 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/SQLServerEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/SQLServerEnvironment.java @@ -24,6 +24,7 @@ import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.guacamole.auth.jdbc.security.PasswordPolicy; +import org.apache.ibatis.session.SqlSession; /** * A SQLServer-specific implementation of JDBCEnvironment provides database @@ -252,7 +253,7 @@ public class SQLServerEnvironment extends JDBCEnvironment { } @Override - public boolean isRecursiveQuerySupported() { + public boolean isRecursiveQuerySupported(SqlSession session) { return true; // All versions of SQL Server support recursive queries through CTEs }