Skip to content

Commit b30c1ac

Browse files
committed
Polishing
Signed-off-by: Nils Bandener <nils.bandener@eliatra.com>
1 parent b865bce commit b30c1ac

File tree

7 files changed

+137
-43
lines changed

7 files changed

+137
-43
lines changed

src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1119,7 +1119,7 @@ public Collection<Object> createComponents(
11191119
interClusterRequestEvaluator = ReflectionHelper.instantiateInterClusterRequestEvaluator(className, settings);
11201120
}
11211121

1122-
UserFactory userFactory = new UserFactory.Caching();
1122+
UserFactory userFactory = new UserFactory.Caching(settings);
11231123

11241124
final PrivilegesInterceptor privilegesInterceptor;
11251125

@@ -2146,6 +2146,10 @@ public List<Setting<?>> getSettings() {
21462146
Property.Filtered
21472147
)
21482148
);
2149+
2150+
settings.add(UserFactory.Caching.MAX_SIZE);
2151+
settings.add(UserFactory.Caching.MAX_ENTRIES);
2152+
settings.add(UserFactory.Caching.EXPIRE_AFTER_ACCESS);
21492153
}
21502154

21512155
return settings;

src/main/java/org/opensearch/security/auth/AuthorizationBackend.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,12 @@ public interface AuthorizationBackend {
5454
String getType();
5555

5656
/**
57-
* Populate a {@link User} with backend roles. This method will not be called for cached users.
58-
* <p/>
59-
* Add them by calling either {@code user.addRole()} or {@code user.addRoles()}
60-
* </P>
57+
* This method is called during the auth phase to add additional backend roles to a {@link User}. This method will not be called for cached users.
58+
* <p>
59+
* Implementations must use the withRoles() method and return the newly created User object.
60+
*
6161
* @param user The authenticated user to populate with backend roles, never null
6262
* @param context Context data specific to the request that is currently processed.
63-
* <em>This parameter is for future usage, currently always empty credentials are passed!</em>
6463
* @throws OpenSearchSecurityException in case when the authorization backend cannot be reached
6564
* or the {@code credentials} are insufficient to authenticate to the authorization backend.
6665
*/

src/main/java/org/opensearch/security/auth/ImpersonationBackend.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
import org.opensearch.security.user.User;
1717

18+
/**
19+
* If an authentication backend class implements this interface, the auth type can be used for impersonation.
20+
*/
1821
public interface ImpersonationBackend {
1922
/**
2023
* The type (name) of the impersonation backend. Only for logging.

src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.opensearch.common.settings.Settings;
3333
import org.opensearch.security.auth.AuthenticationBackend;
3434
import org.opensearch.security.auth.AuthenticationContext;
35-
import org.opensearch.security.auth.AuthenticationContext;
3635
import org.opensearch.security.auth.ImpersonationBackend;
3736
import org.opensearch.security.user.AuthCredentials;
3837
import org.opensearch.security.user.User;

src/main/java/org/opensearch/security/user/User.java

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,16 @@
4343
import com.google.common.collect.ImmutableMap;
4444
import com.google.common.collect.ImmutableSet;
4545

46+
import org.opensearch.OpenSearchException;
4647
import org.opensearch.security.support.Base64Helper;
4748

4849
/**
49-
* A authenticated user and attributes associated to them (like roles, tenant, custom attributes)
50-
* <p/>
51-
* <b>Do not subclass from this class!</b>
50+
* An authenticated user and attributes associated to them (like roles, tenant, custom attributes).
51+
* <p>
52+
* Objects of this class are immutable. Any method that modifies an attribute, returns a modified copy of this object.
53+
* As the objects are immutable, all operations on the objects are inherently thread-safe. No external synchronization is required.
54+
* <p>
55+
* <b>Do not subclass from this class; do not add attributes that can be modified using publicly visible methods!</b>
5256
*
5357
*/
5458
public class User implements Serializable, CustomAttributesAware {
@@ -66,8 +70,12 @@ public class User implements Serializable, CustomAttributesAware {
6670

6771
/**
6872
* Deserializes the given serialized from of a user object and returns the actual user object.
69-
*
73+
* <p>
7074
* Note: Instead of using this method, prefer to use UserFactory.Caching to benefit from already parsed user objects.
75+
*
76+
* @param serializedBase64 a string with a serialized form of a User object
77+
* @return A User object. Never returns null.
78+
* @throws OpenSearchException in case the provided string could not be processed.
7179
*/
7280
public static User fromSerializedBase64(String serializedBase64) {
7381
User user = (User) Base64Helper.deserializeObject(serializedBase64);
@@ -86,26 +94,13 @@ public static User fromSerializedBase64(String serializedBase64) {
8694
private final String requestedTenant;
8795
private final ImmutableMap<String, String> attributes;
8896
private final boolean isInjected;
89-
private volatile transient String serializedBase64;
97+
private final transient int estimatedByteSize;
9098

9199
/**
92-
* Create a new authenticated user
93-
*
94-
* @param name The username (must not be null or empty)
95-
* @param roles Roles of which the user is a member off (maybe null)
96-
* @param customAttributes Custom attributes associated with this (maybe null)
97-
* @throws IllegalArgumentException if name is null or empty
100+
* This attribute caches the serialized form of the User object. As the User object is immutable,
101+
* this value can be re-used when it is set.
98102
*/
99-
public User(final String name, final Collection<String> roles, final AuthCredentials customAttributes) {
100-
this(
101-
name,
102-
ImmutableSet.copyOf(roles),
103-
ImmutableSet.of(),
104-
null,
105-
customAttributes != null ? ImmutableMap.copyOf(customAttributes.getAttributes()) : ImmutableMap.of(),
106-
false
107-
);
108-
}
103+
private volatile transient String serializedBase64;
109104

110105
/**
111106
* Create a new authenticated user without roles and attributes
@@ -114,9 +109,19 @@ public User(final String name, final Collection<String> roles, final AuthCredent
114109
* @throws IllegalArgumentException if name is null or empty
115110
*/
116111
public User(final String name) {
117-
this(name, ImmutableSet.of(), null);
112+
this(name, ImmutableSet.of(), ImmutableSet.of(), null, ImmutableMap.of(), false);
118113
}
119114

115+
/**
116+
* Creates a new User object. This is the main constructor, prefer using this one.
117+
*
118+
* @param name The username; must not be null or an empty string.
119+
* @param roles The backend roles of a user. Must not be null. For empty roles, pass ImmutableSet.of().
120+
* @param securityRoles The security roles of a user. Must not be null. For empty roles, pass ImmutableSet.of().
121+
* @param requestedTenant The requested tenant property of the user. May be null.
122+
* @param attributes The user attributes. Must not be null. For no attributes, pass ImmutableMap.of()
123+
* @param isInjected A flag that indicates whether the user was injected.
124+
*/
120125
public User(
121126
String name,
122127
ImmutableSet<String> roles,
@@ -135,6 +140,7 @@ public User(
135140
this.requestedTenant = requestedTenant;
136141
this.attributes = Objects.requireNonNull(attributes);
137142
this.isInjected = isInjected;
143+
this.estimatedByteSize = calcEstimatedByteSize();
138144
}
139145

140146
public final String getName() {
@@ -150,9 +156,7 @@ public ImmutableSet<String> getRoles() {
150156
}
151157

152158
/**
153-
* Associate this user with a backend role
154-
*
155-
* @param role The backend role
159+
* Returns a new User object that additionally contains the provided backend role.
156160
*/
157161
public User withRole(String role) {
158162
return new User(
@@ -166,9 +170,7 @@ public User withRole(String role) {
166170
}
167171

168172
/**
169-
* Associate this user with a set of backend roles
170-
*
171-
* @param roles The backend roles
173+
* Returns a new User object that additionally contains the provided backend roles.
172174
*/
173175
public User withRoles(Collection<String> roles) {
174176
if (roles == null || roles.isEmpty()) {
@@ -186,9 +188,7 @@ public User withRoles(Collection<String> roles) {
186188
}
187189

188190
/**
189-
* Associate this user with a set of custom attributes
190-
*
191-
* @param attributes custom attributes
191+
* Returns a new User object that additionally contains the provided map of custom attributes
192192
*/
193193
public User withAttributes(Map<String, String> attributes) {
194194
if (attributes == null || attributes.isEmpty()) {
@@ -209,6 +209,9 @@ public final String getRequestedTenant() {
209209
return requestedTenant;
210210
}
211211

212+
/**
213+
* Returns a new User object with the requestedTenant attribute set to the supplied value.
214+
*/
212215
public User withRequestedTenant(String requestedTenant) {
213216
if (Objects.equals(requestedTenant, this.requestedTenant)) {
214217
return this;
@@ -277,6 +280,9 @@ public ImmutableMap<String, String> getCustomAttributesMap() {
277280
return this.attributes;
278281
}
279282

283+
/**
284+
* Returns a new user object that additionally contains the given security roles.
285+
*/
280286
public User withSecurityRoles(Collection<String> securityRoles) {
281287
if (securityRoles == null || securityRoles.isEmpty()) {
282288
return this;
@@ -315,6 +321,9 @@ public boolean isPluginUser() {
315321
return name != null && name.startsWith("plugin:");
316322
}
317323

324+
/**
325+
* Returns a String containing serialized form of this User object. Never returns null.
326+
*/
318327
public String toSerializedBase64() {
319328
String result = this.serializedBase64;
320329

@@ -325,11 +334,42 @@ public String toSerializedBase64() {
325334
return result;
326335
}
327336

337+
/**
338+
* Returns a rough estimated byte size of this object. Used for cache size control.
339+
*/
340+
public int estimatedByteSize() {
341+
return this.estimatedByteSize;
342+
}
343+
344+
private int calcEstimatedByteSize() {
345+
int size = 32;
346+
size += estimateStringSize(this.name);
347+
size += estimateStringSize(this.requestedTenant);
348+
size += this.roles.stream().mapToInt(User::estimateStringSize).sum() + 32;
349+
size += this.securityRoles.stream().mapToInt(User::estimateStringSize).sum() + 32;
350+
size += this.attributes.entrySet()
351+
.stream()
352+
.mapToInt((entry) -> estimateStringSize(entry.getKey()) + estimateStringSize(entry.getValue()))
353+
.sum() + 32;
354+
return size;
355+
}
356+
357+
private static int estimateStringSize(String s) {
358+
if (s != null) {
359+
return 40 + s.length() * 2;
360+
} else {
361+
return 0;
362+
}
363+
}
364+
328365
void readObject(ObjectInputStream stream) throws InvalidObjectException {
329366
// This object is not supposed to directly read in order to keep compatibility with older OpenSearch versions
330367
throw new InvalidObjectException("Use org.opensearch.security.user.serialized.User");
331368
}
332369

370+
/**
371+
* Used for creating a backwards compatible object that can be used for serialization.
372+
*/
333373
@Serial
334374
private static final ObjectStreamField[] serialPersistentFields = {
335375
new ObjectStreamField("name", String.class),

src/main/java/org/opensearch/security/user/UserFactory.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515

1616
import com.google.common.cache.Cache;
1717
import com.google.common.cache.CacheBuilder;
18+
import com.google.common.cache.Weigher;
19+
20+
import org.opensearch.OpenSearchException;
21+
import org.opensearch.common.settings.Setting;
22+
import org.opensearch.common.settings.Settings;
23+
import org.opensearch.common.unit.TimeValue;
24+
import org.opensearch.core.common.unit.ByteSizeUnit;
25+
import org.opensearch.core.common.unit.ByteSizeValue;
1826

1927
/**
2028
* Infrastructure to create user objects. This class has two implementations:
@@ -24,6 +32,13 @@
2432
* </ul>
2533
*/
2634
public abstract class UserFactory {
35+
/**
36+
* Converts a serialized form of a User object to a User obect. This might use a cache.
37+
*
38+
* @param serializedBase64 a string with a serialized form of a User object
39+
* @return A User object. Never returns null.
40+
* @throws OpenSearchException in case the provided string could not be processed.
41+
*/
2742
public abstract User fromSerializedBase64(String serializedBase64);
2843

2944
public static class Simple extends UserFactory {
@@ -35,10 +50,45 @@ public User fromSerializedBase64(String serializedBase64) {
3550
}
3651

3752
public static class Caching extends UserFactory {
53+
54+
/**
55+
* This setting specifies the maximum estimated byte size of the cache. The size is estimated, so take it
56+
* with a grain of salt. It shall just serve as a rough limit. The default is 10 MB.
57+
*/
58+
public static Setting<ByteSizeValue> MAX_SIZE = Setting.memorySizeSetting(
59+
"plugins.security.transport_user_cache.max_heap_size",
60+
new ByteSizeValue(10, ByteSizeUnit.MB),
61+
Setting.Property.NodeScope
62+
);
63+
64+
/**
65+
* This setting specifies the maximum number of users inside the cache. The default is 1000 users.
66+
*/
67+
public static Setting<Integer> MAX_ENTRIES = Setting.intSetting(
68+
"plugins.security.transport_user_cache.max_entries",
69+
1000,
70+
Setting.Property.NodeScope
71+
);
72+
73+
/**
74+
* This setting specifies the maximum time an entry is kept in the cache. This is solely for saving space;
75+
* a stale cache is not possible. The default is 1 hour.
76+
*/
77+
public static Setting<TimeValue> EXPIRE_AFTER_ACCESS = Setting.timeSetting(
78+
"plugins.security.transport_user_cache.expire_after_access",
79+
TimeValue.timeValueHours(1),
80+
Setting.Property.NodeScope
81+
);
82+
3883
private final Cache<String, User> serializedBase64ToUserCache;
3984

40-
public Caching() {
41-
this.serializedBase64ToUserCache = CacheBuilder.newBuilder().expireAfterAccess(Duration.ofHours(1)).build();
85+
public Caching(Settings settings) {
86+
this.serializedBase64ToUserCache = CacheBuilder.<String, User>newBuilder()
87+
.weigher((Weigher<String, User>) (key, user) -> 16 + key.length() + user.estimatedByteSize())
88+
.maximumSize(MAX_ENTRIES.get(settings))
89+
.maximumWeight(MAX_SIZE.get(settings).getBytes())
90+
.expireAfterAccess(Duration.ofMillis(EXPIRE_AFTER_ACCESS.get(settings).millis()))
91+
.build();
4292
}
4393

4494
public User fromSerializedBase64(String serializedBase64) {

src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
package org.opensearch.security.identity;
1313

1414
import java.io.IOException;
15-
import java.util.List;
1615
import java.util.Set;
1716

1817
import org.junit.After;
@@ -202,7 +201,7 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception {
202201
doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName();
203202
doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed();
204203
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
205-
threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null));
204+
threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon"));
206205
when(threadPool.getThreadContext()).thenReturn(threadContext);
207206
final ConfigModel configModel = mock(ConfigModel.class);
208207
tokenManager.onConfigModelChanged(configModel);
@@ -228,7 +227,7 @@ public void issueOnBehalfOfToken_success() throws Exception {
228227
doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName();
229228
doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed();
230229
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
231-
threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null));
230+
threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon"));
232231
when(threadPool.getThreadContext()).thenReturn(threadContext);
233232
final ConfigModel configModel = mock(ConfigModel.class);
234233
tokenManager.onConfigModelChanged(configModel);

0 commit comments

Comments
 (0)