Skip to content

Commit de10c83

Browse files
committed
fix: harden SQL metadata context and identifier resolution
1 parent 4b10013 commit de10c83

3 files changed

Lines changed: 127 additions & 22 deletions

File tree

backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ConnectionManager.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public interface ConnectionManager {
2020

2121
void setDatabaseContext(String database);
2222

23+
<T> T withDatabaseContext(String database, DatabaseContextAction<T> action) throws SQLException;
24+
2325
Datasource getDatasource();
2426

2527
DBConnectInfo getConnectInfo();
@@ -37,4 +39,9 @@ public interface ConnectionManager {
3739
String getDatabaseContextKey();
3840

3941
void close();
42+
43+
@FunctionalInterface
44+
interface DatabaseContextAction<T> {
45+
T run() throws SQLException;
46+
}
4047
}

backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,26 @@ public Driver getDriver() {
7676

7777
@Override
7878
public void setDatabaseContext(String database) {
79-
this.currentDatabase.set(database);
79+
if (StringUtils.isBlank(database)) {
80+
this.currentDatabase.remove();
81+
} else {
82+
this.currentDatabase.set(database);
83+
}
84+
}
85+
86+
@Override
87+
public <T> T withDatabaseContext(String database, DatabaseContextAction<T> action) throws SQLException {
88+
var previousDatabase = this.currentDatabase.get();
89+
try {
90+
this.setDatabaseContext(database);
91+
return action.run();
92+
} finally {
93+
if (previousDatabase == null) {
94+
this.currentDatabase.remove();
95+
} else {
96+
this.currentDatabase.set(previousDatabase);
97+
}
98+
}
8099
}
81100

82101
@Override

backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/SqlMetadataCatalog.java

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88

99
import java.sql.SQLException;
1010
import java.util.ArrayList;
11+
import java.util.Collection;
1112
import java.util.Comparator;
1213
import java.util.LinkedHashMap;
1314
import java.util.List;
1415
import java.util.Locale;
1516
import java.util.Map;
1617
import java.util.Objects;
1718
import java.util.Set;
18-
import java.util.function.Function;
19-
import java.util.stream.Collectors;
2019

2120
public class SqlMetadataCatalog {
2221
public static final int DEFAULT_RELATION_LIMIT = 100;
@@ -35,7 +34,14 @@ public SqlMetadataCatalog(ResourceBrowser resourceBrowser, ConnectionManager con
3534

3635
public RelationMetadataPage listRelations(ResourceNodeSnapshot node, String context, String prefix, Integer limit)
3736
throws SQLException {
38-
var scope = this.resolveScope(node, context);
37+
var scope = connectionManager.withDatabaseContext(null, () -> this.resolveScope(node, context));
38+
return connectionManager.withDatabaseContext(
39+
scope.catalog(),
40+
() -> this.listRelations(scope, prefix, limit)
41+
);
42+
}
43+
44+
private RelationMetadataPage listRelations(MetadataScope scope, String prefix, Integer limit) throws SQLException {
3945
var schemas = this.resolveSchemas(scope.schema());
4046
var relations = new ArrayList<QualifiedRelation>();
4147

@@ -80,20 +86,29 @@ public List<RelationColumnsMetadata> listColumns(
8086
throw new IllegalArgumentException("Too many relations in one metadata request");
8187
}
8288

83-
var scope = this.resolveScope(node, context);
89+
var scope = connectionManager.withDatabaseContext(null, () -> this.resolveScope(node, context));
90+
return connectionManager.withDatabaseContext(
91+
scope.catalog(),
92+
() -> this.listColumns(scope, requestedRelations)
93+
);
94+
}
95+
96+
private List<RelationColumnsMetadata> listColumns(
97+
MetadataScope scope,
98+
List<QualifiedRelation> requestedRelations
99+
) throws SQLException {
84100
var availableSchemas = this.resolveSchemas(null);
85-
var canonicalSchemas = availableSchemas.stream()
86-
.collect(Collectors.toMap(Function.identity(), Function.identity(), (left, right) -> left));
87101
var relationsBySchema = new LinkedHashMap<String, Map<RelationKey, QualifiedRelation>>();
88102
var canonicalRequests = new LinkedHashMap<RelationKey, QualifiedRelation>();
89103

90104
for (var requested : requestedRelations) {
91105
this.validateRequestedRelation(requested, scope.catalog());
92106
var requestedSchema = StringUtils.defaultIfBlank(requested.schema(), scope.schema());
93-
var canonicalSchema = canonicalSchemas.get(requestedSchema);
94-
if (canonicalSchema == null) {
95-
throw new IllegalArgumentException("Unknown relation schema");
96-
}
107+
var canonicalSchema = this.resolveCanonicalIdentifier(
108+
availableSchemas,
109+
requestedSchema,
110+
"Unknown relation schema"
111+
);
97112

98113
var availableRelations = relationsBySchema.get(canonicalSchema);
99114
if (availableRelations == null) {
@@ -103,9 +118,18 @@ public List<RelationColumnsMetadata> listColumns(
103118
var key = new RelationKey(canonicalSchema, requested.name(), requested.kind());
104119
var canonical = availableRelations.get(key);
105120
if (canonical == null) {
106-
throw new IllegalArgumentException("Unknown relation");
121+
var canonicalName = this.resolveCanonicalIdentifier(
122+
availableRelations.values().stream()
123+
.filter(relation -> relation.kind().equals(requested.kind()))
124+
.map(QualifiedRelation::name)
125+
.toList(),
126+
requested.name(),
127+
"Unknown relation"
128+
);
129+
canonical = availableRelations.get(new RelationKey(canonicalSchema, canonicalName, requested.kind()));
107130
}
108-
canonicalRequests.putIfAbsent(key, canonical);
131+
var canonicalKey = new RelationKey(canonical.schema(), canonical.name(), canonical.kind());
132+
canonicalRequests.putIfAbsent(canonicalKey, canonical);
109133
}
110134

111135
var result = new ArrayList<RelationColumnsMetadata>();
@@ -136,10 +160,7 @@ private MetadataScope resolveScope(ResourceNodeSnapshot node, String context) th
136160
}
137161
catalog = currentContext;
138162
}
139-
if (StringUtils.isNotBlank(catalog)) {
140-
SqlIdentifierUtils.validateDatabaseName(catalog);
141-
connectionManager.setDatabaseContext(catalog);
142-
}
163+
SqlIdentifierUtils.validateDatabaseName(catalog);
143164

144165
String schema = null;
145166
if (StringUtils.equals(contextKey, "schema")) {
@@ -158,11 +179,58 @@ private List<String> resolveSchemas(String requestedSchema) throws SQLException
158179
if (StringUtils.isBlank(requestedSchema)) {
159180
return schemas;
160181
}
161-
return schemas.stream()
162-
.filter(schema -> schema.equals(requestedSchema))
163-
.findFirst()
164-
.map(List::of)
165-
.orElseThrow(() -> new IllegalArgumentException("Unknown metadata schema"));
182+
return List.of(this.resolveCanonicalIdentifier(schemas, requestedSchema, "Unknown metadata schema"));
183+
}
184+
185+
private String resolveCanonicalIdentifier(
186+
Collection<String> candidates,
187+
String requested,
188+
String unknownMessage
189+
) {
190+
if (StringUtils.isBlank(requested)) {
191+
throw new IllegalArgumentException(unknownMessage);
192+
}
193+
194+
var exactMatches = candidates.stream().filter(requested::equals).distinct().toList();
195+
if (exactMatches.size() == 1) {
196+
return exactMatches.get(0);
197+
}
198+
199+
var caseRule = this.identifierCaseRule();
200+
if (caseRule == IdentifierCaseRule.LOWER || caseRule == IdentifierCaseRule.UPPER) {
201+
var normalizedRequested = caseRule.normalize(requested);
202+
var normalizedMatches = candidates.stream()
203+
.filter(candidate -> candidate.equals(caseRule.normalize(candidate)))
204+
.filter(candidate -> candidate.equals(normalizedRequested))
205+
.distinct()
206+
.toList();
207+
if (normalizedMatches.size() == 1) {
208+
return normalizedMatches.get(0);
209+
}
210+
throw new IllegalArgumentException(unknownMessage);
211+
}
212+
213+
if (caseRule == IdentifierCaseRule.INSENSITIVE) {
214+
var insensitiveMatches = candidates.stream()
215+
.filter(candidate -> candidate.equalsIgnoreCase(requested))
216+
.distinct()
217+
.toList();
218+
if (insensitiveMatches.size() == 1) {
219+
return insensitiveMatches.get(0);
220+
}
221+
}
222+
throw new IllegalArgumentException(unknownMessage);
223+
}
224+
225+
private IdentifierCaseRule identifierCaseRule() {
226+
var connectInfo = connectionManager.getConnectInfo();
227+
var dbType = connectInfo == null ? "" : StringUtils.defaultString(connectInfo.getDbType());
228+
return switch (dbType.toLowerCase(Locale.ROOT)) {
229+
case "postgresql" -> IdentifierCaseRule.LOWER;
230+
case "oracle", "db2", "dm", "dameng" -> IdentifierCaseRule.UPPER;
231+
case "mysql", "mariadb", "sqlserver" -> IdentifierCaseRule.INSENSITIVE;
232+
default -> IdentifierCaseRule.EXACT;
233+
};
166234
}
167235

168236
private Map<RelationKey, QualifiedRelation> loadRelationsByKey(String catalog, String schema) throws SQLException {
@@ -193,4 +261,15 @@ private record MetadataScope(String catalog, String schema) {
193261

194262
private record RelationKey(String schema, String name, String kind) {
195263
}
264+
265+
private enum IdentifierCaseRule {
266+
LOWER,
267+
UPPER,
268+
INSENSITIVE,
269+
EXACT;
270+
271+
private String normalize(String identifier) {
272+
return this == UPPER ? identifier.toUpperCase(Locale.ROOT) : identifier.toLowerCase(Locale.ROOT);
273+
}
274+
}
196275
}

0 commit comments

Comments
 (0)