4343import com .google .common .collect .ImmutableMap ;
4444import com .google .common .collect .ImmutableSet ;
4545
46+ import org .opensearch .OpenSearchException ;
4647import 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 */
5458public 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 ),
0 commit comments