Skip to content

Commit 0c7de7f

Browse files
committed
Security: reduce memory usage of DnRoleMapper (#34250)
The `DnRoleMapper` class is used to map distinguished names of groups and users to role names. This mapper builds in an internal map that maps from a `com.unboundid.ldap.sdk.DN` to a `Set<String>`. In cases where a lot of distinct DNs are mapped to roles, this can consume quite a bit of memory. The majority of the memory is consumed by the DN object. For example, a 94 character DN that has 9 relative DNs (RDN) will retain 4KB of memory, whereas the String itself consumes less than 250 bytes. In order to reduce memory usage, we can map from a normalized DN string to a List of roles. The normalized string is actually how the DN class determines equality with another DN and we can drop the overhead of needing to keep all of the other objects in memory. Additionally the use of a List provides memory savings as each HashSet is backed by a HashMap, which consumes a great deal more memory than an appropriately sized ArrayList. The uniqueness we get from a Set is maintained by first building a set when parsing the file and then converting to a list upon completion. Closes #34237
1 parent 9055797 commit 0c7de7f

File tree

2 files changed

+26
-19
lines changed

2 files changed

+26
-19
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
import java.io.IOException;
99
import java.nio.file.Files;
1010
import java.nio.file.Path;
11+
import java.util.ArrayList;
1112
import java.util.Collection;
1213
import java.util.Collections;
1314
import java.util.HashMap;
1415
import java.util.HashSet;
16+
import java.util.List;
1517
import java.util.Map;
1618
import java.util.Objects;
1719
import java.util.Set;
1820
import java.util.concurrent.CopyOnWriteArrayList;
21+
import java.util.stream.Collectors;
1922

2023
import com.unboundid.ldap.sdk.DN;
2124
import com.unboundid.ldap.sdk.LDAPException;
@@ -52,7 +55,7 @@ public class DnRoleMapper implements UserRoleMapper {
5255
private final Path file;
5356
private final boolean useUnmappedGroupsAsRoles;
5457
private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();
55-
private volatile Map<DN, Set<String>> dnRoles;
58+
private volatile Map<String, List<String>> dnRoles;
5659

5760
public DnRoleMapper(RealmConfig config, ResourceWatcherService watcherService) {
5861
this.config = config;
@@ -88,7 +91,7 @@ public static Path resolveFile(Settings settings, Environment env) {
8891
* logging the error and skipping/removing all mappings. This is aligned with how we handle other auto-loaded files
8992
* in security.
9093
*/
91-
public static Map<DN, Set<String>> parseFileLenient(Path path, Logger logger, String realmType, String realmName) {
94+
public static Map<String, List<String>> parseFileLenient(Path path, Logger logger, String realmType, String realmName) {
9295
try {
9396
return parseFile(path, logger, realmType, realmName, false);
9497
} catch (Exception e) {
@@ -99,7 +102,7 @@ public static Map<DN, Set<String>> parseFileLenient(Path path, Logger logger, St
99102
}
100103
}
101104

102-
public static Map<DN, Set<String>> parseFile(Path path, Logger logger, String realmType, String realmName, boolean strict) {
105+
public static Map<String, List<String>> parseFile(Path path, Logger logger, String realmType, String realmName, boolean strict) {
103106

104107
logger.trace("reading realm [{}/{}] role mappings file [{}]...", realmType, realmName, path.toAbsolutePath());
105108

@@ -150,7 +153,10 @@ public static Map<DN, Set<String>> parseFile(Path path, Logger logger, String re
150153

151154
logger.debug("[{}] role mappings found in file [{}] for realm [{}/{}]", dnToRoles.size(), path.toAbsolutePath(), realmType,
152155
realmName);
153-
return unmodifiableMap(dnToRoles);
156+
Map<String, List<String>> normalizedMap = dnToRoles.entrySet().stream().collect(Collectors.toMap(
157+
entry -> entry.getKey().toNormalizedString(),
158+
entry -> Collections.unmodifiableList(new ArrayList<>(entry.getValue()))));
159+
return unmodifiableMap(normalizedMap);
154160
} catch (IOException | SettingsException e) {
155161
throw new ElasticsearchException("could not read realm [" + realmType + "/" + realmName + "] role mappings file [" +
156162
path.toAbsolutePath() + "]", e);
@@ -177,8 +183,9 @@ public Set<String> resolveRoles(String userDnString, Collection<String> groupDns
177183
Set<String> roles = new HashSet<>();
178184
for (String groupDnString : groupDns) {
179185
DN groupDn = dn(groupDnString);
180-
if (dnRoles.containsKey(groupDn)) {
181-
roles.addAll(dnRoles.get(groupDn));
186+
String normalizedGroupDn = groupDn.toNormalizedString();
187+
if (dnRoles.containsKey(normalizedGroupDn)) {
188+
roles.addAll(dnRoles.get(normalizedGroupDn));
182189
} else if (useUnmappedGroupsAsRoles) {
183190
roles.add(relativeName(groupDn));
184191
}
@@ -188,14 +195,14 @@ public Set<String> resolveRoles(String userDnString, Collection<String> groupDns
188195
groupDns, file.getFileName(), config.type(), config.name());
189196
}
190197

191-
DN userDn = dn(userDnString);
192-
Set<String> rolesMappedToUserDn = dnRoles.get(userDn);
198+
String normalizedUserDn = dn(userDnString).toNormalizedString();
199+
List<String> rolesMappedToUserDn = dnRoles.get(normalizedUserDn);
193200
if (rolesMappedToUserDn != null) {
194201
roles.addAll(rolesMappedToUserDn);
195202
}
196203
if (logger.isDebugEnabled()) {
197204
logger.debug("the roles [{}], are mapped from the user [{}] using file [{}] for realm [{}/{}]",
198-
(rolesMappedToUserDn == null) ? Collections.emptySet() : rolesMappedToUserDn, userDnString, file.getFileName(),
205+
(rolesMappedToUserDn == null) ? Collections.emptySet() : rolesMappedToUserDn, normalizedUserDn, file.getFileName(),
199206
config.type(), config.name());
200207
}
201208
return roles;

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -200,27 +200,27 @@ public void testAddNullListener() throws Exception {
200200
public void testParseFile() throws Exception {
201201
Path file = getDataPath("role_mapping.yml");
202202
Logger logger = CapturingLogger.newCapturingLogger(Level.INFO);
203-
Map<DN, Set<String>> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false);
203+
Map<String, List<String>> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false);
204204
assertThat(mappings, notNullValue());
205205
assertThat(mappings.size(), is(3));
206206

207207
DN dn = new DN("cn=avengers,ou=marvel,o=superheros");
208-
assertThat(mappings, hasKey(dn));
209-
Set<String> roles = mappings.get(dn);
208+
assertThat(mappings, hasKey(dn.toNormalizedString()));
209+
List<String> roles = mappings.get(dn.toNormalizedString());
210210
assertThat(roles, notNullValue());
211211
assertThat(roles, hasSize(2));
212212
assertThat(roles, containsInAnyOrder("security", "avenger"));
213213

214214
dn = new DN("cn=shield,ou=marvel,o=superheros");
215-
assertThat(mappings, hasKey(dn));
216-
roles = mappings.get(dn);
215+
assertThat(mappings, hasKey(dn.toNormalizedString()));
216+
roles = mappings.get(dn.toNormalizedString());
217217
assertThat(roles, notNullValue());
218218
assertThat(roles, hasSize(1));
219219
assertThat(roles, contains("security"));
220220

221221
dn = new DN("cn=Horatio Hornblower,ou=people,o=sevenSeas");
222-
assertThat(mappings, hasKey(dn));
223-
roles = mappings.get(dn);
222+
assertThat(mappings, hasKey(dn.toNormalizedString()));
223+
roles = mappings.get(dn.toNormalizedString());
224224
assertThat(roles, notNullValue());
225225
assertThat(roles, hasSize(1));
226226
assertThat(roles, contains("avenger"));
@@ -230,7 +230,7 @@ public void testParseFile_Empty() throws Exception {
230230
Path file = createTempDir().resolve("foo.yaml");
231231
Files.createFile(file);
232232
Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG);
233-
Map<DN, Set<String>> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false);
233+
Map<String, List<String>> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false);
234234
assertThat(mappings, notNullValue());
235235
assertThat(mappings.isEmpty(), is(true));
236236
List<String> events = CapturingLogger.output(logger.getName(), Level.DEBUG);
@@ -241,7 +241,7 @@ public void testParseFile_Empty() throws Exception {
241241
public void testParseFile_WhenFileDoesNotExist() throws Exception {
242242
Path file = createTempDir().resolve(randomAlphaOfLength(10));
243243
Logger logger = CapturingLogger.newCapturingLogger(Level.INFO);
244-
Map<DN, Set<String>> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false);
244+
Map<String, List<String>> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false);
245245
assertThat(mappings, notNullValue());
246246
assertThat(mappings.isEmpty(), is(true));
247247

@@ -271,7 +271,7 @@ public void testParseFileLenient_WhenCannotReadFile() throws Exception {
271271
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
272272
Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16);
273273
Logger logger = CapturingLogger.newCapturingLogger(Level.INFO);
274-
Map<DN, Set<String>> mappings = DnRoleMapper.parseFileLenient(file, logger, "_type", "_name");
274+
Map<String, List<String>> mappings = DnRoleMapper.parseFileLenient(file, logger, "_type", "_name");
275275
assertThat(mappings, notNullValue());
276276
assertThat(mappings.isEmpty(), is(true));
277277
List<String> events = CapturingLogger.output(logger.getName(), Level.ERROR);

0 commit comments

Comments
 (0)