mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-220: Dynamically detect whether the MariaDB / MySQL server supports recursive CTEs.
This commit is contained in:
@@ -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);
|
||||
|
||||
}
|
||||
|
@@ -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<String> selectEffectiveGroupIdentifiers(@Param("entity") EntityModel entity,
|
||||
@Param("effectiveGroups") Collection<String> effectiveGroups);
|
||||
@Param("effectiveGroups") Collection<String> effectiveGroups,
|
||||
@Param("recursive") boolean recursive);
|
||||
|
||||
}
|
||||
|
@@ -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<String> retrieveEffectiveGroups(ModeledPermissions<? extends EntityModel> entity,
|
||||
Collection<String> effectiveGroups) {
|
||||
|
||||
// Retrieve the effective user groups of the given entity, recursively if possible
|
||||
Set<String> identifiers = entityMapper.selectEffectiveGroupIdentifiers(entity.getModel(), effectiveGroups);
|
||||
boolean recursive = environment.isRecursiveQuerySupported(sqlSession);
|
||||
Set<String> 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<String> previousIdentifiers;
|
||||
do {
|
||||
previousIdentifiers = identifiers;
|
||||
identifiers = entityMapper.selectEffectiveGroupIdentifiers(entity.getModel(), previousIdentifiers);
|
||||
identifiers = entityMapper.selectEffectiveGroupIdentifiers(entity.getModel(), previousIdentifiers, false);
|
||||
} while (identifiers.size() > previousIdentifiers.size());
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -65,43 +65,76 @@
|
||||
<!-- Select names of all effective groups (including inherited) -->
|
||||
<select id="selectEffectiveGroupIdentifiers" resultType="string">
|
||||
|
||||
WITH RECURSIVE related_entity(entity_id) AS (
|
||||
SELECT
|
||||
guacamole_user_group.entity_id
|
||||
FROM guacamole_user_group
|
||||
JOIN guacamole_user_group_member ON guacamole_user_group.user_group_id = guacamole_user_group_member.user_group_id
|
||||
WHERE
|
||||
<if test="!recursive">
|
||||
SELECT
|
||||
guacamole_entity.name
|
||||
FROM guacamole_user_group
|
||||
JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
|
||||
JOIN guacamole_user_group_member ON guacamole_user_group.user_group_id = guacamole_user_group_member.user_group_id
|
||||
WHERE
|
||||
guacamole_user_group.disabled = false
|
||||
AND (
|
||||
guacamole_user_group_member.member_entity_id = #{entity.entityID}
|
||||
AND guacamole_user_group.disabled = false
|
||||
<if test="!effectiveGroups.isEmpty()">
|
||||
<if test="!effectiveGroups.isEmpty()">
|
||||
OR guacamole_user_group_member.member_entity_id IN (
|
||||
SELECT entity_id FROM guacamole_entity
|
||||
WHERE type = 'USER_GROUP' AND name IN
|
||||
<foreach collection="effectiveGroups" item="effectiveGroup"
|
||||
open="(" separator="," close=")">
|
||||
#{effectiveGroup,jdbcType=VARCHAR}
|
||||
</foreach>
|
||||
)
|
||||
OR guacamole_user_group.entity_id IN (
|
||||
SELECT entity_id FROM guacamole_entity
|
||||
WHERE type = 'USER_GROUP' AND name IN
|
||||
<foreach collection="effectiveGroups" item="effectiveGroup"
|
||||
open="(" separator="," close=")">
|
||||
#{effectiveGroup,jdbcType=VARCHAR}
|
||||
</foreach>
|
||||
)
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
|
||||
<if test="recursive">
|
||||
WITH RECURSIVE related_entity(entity_id) AS (
|
||||
SELECT
|
||||
guacamole_user_group.entity_id
|
||||
FROM guacamole_user_group
|
||||
JOIN guacamole_user_group_member ON guacamole_user_group.user_group_id = guacamole_user_group_member.user_group_id
|
||||
WHERE
|
||||
guacamole_user_group_member.member_entity_id = #{entity.entityID}
|
||||
AND guacamole_user_group.disabled = false
|
||||
<if test="!effectiveGroups.isEmpty()">
|
||||
UNION
|
||||
SELECT
|
||||
guacamole_entity.entity_id
|
||||
FROM guacamole_entity
|
||||
JOIN guacamole_user_group ON guacamole_user_group.entity_id = guacamole_entity.entity_id
|
||||
WHERE
|
||||
type = 'USER_GROUP'
|
||||
AND name IN
|
||||
<foreach collection="effectiveGroups" item="effectiveGroup"
|
||||
open="(" separator="," close=")">
|
||||
#{effectiveGroup,jdbcType=VARCHAR}
|
||||
</foreach>
|
||||
AND guacamole_user_group.disabled = false
|
||||
</if>
|
||||
UNION
|
||||
SELECT
|
||||
guacamole_entity.entity_id
|
||||
FROM guacamole_entity
|
||||
JOIN guacamole_user_group ON guacamole_user_group.entity_id = guacamole_entity.entity_id
|
||||
guacamole_user_group.entity_id
|
||||
FROM related_entity
|
||||
JOIN guacamole_user_group_member ON related_entity.entity_id = guacamole_user_group_member.member_entity_id
|
||||
JOIN guacamole_user_group ON guacamole_user_group.user_group_id = guacamole_user_group_member.user_group_id
|
||||
WHERE
|
||||
type = 'USER_GROUP'
|
||||
AND name IN
|
||||
<foreach collection="effectiveGroups" item="effectiveGroup"
|
||||
open="(" separator="," close=")">
|
||||
#{effectiveGroup,jdbcType=VARCHAR}
|
||||
</foreach>
|
||||
AND guacamole_user_group.disabled = false
|
||||
</if>
|
||||
UNION
|
||||
SELECT
|
||||
guacamole_user_group.entity_id
|
||||
FROM related_entity
|
||||
JOIN guacamole_user_group_member ON related_entity.entity_id = guacamole_user_group_member.member_entity_id
|
||||
JOIN guacamole_user_group ON guacamole_user_group.user_group_id = guacamole_user_group_member.user_group_id
|
||||
WHERE
|
||||
guacamole_user_group.disabled = false
|
||||
)
|
||||
SELECT name
|
||||
FROM related_entity
|
||||
JOIN guacamole_entity ON related_entity.entity_id = guacamole_entity.entity_id
|
||||
WHERE
|
||||
guacamole_entity.type = 'USER_GROUP';
|
||||
guacamole_user_group.disabled = false
|
||||
)
|
||||
SELECT name
|
||||
FROM related_entity
|
||||
JOIN guacamole_entity ON related_entity.entity_id = guacamole_entity.entity_id
|
||||
WHERE
|
||||
guacamole_entity.type = 'USER_GROUP';
|
||||
</if>
|
||||
|
||||
</select>
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user