View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.user2;
20  
21  import java.io.Serializable;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Date;
26  import java.util.HashSet;
27  import java.util.Objects;
28  import java.util.Optional;
29  import java.util.Set;
30  import java.util.SortedSet;
31  import java.util.TreeSet;
32  import java.util.stream.Collectors;
33  import java.util.stream.Stream;
34  
35  import org.hibernate.annotations.SortNatural;
36  import org.mycore.common.MCRException;
37  import org.mycore.common.MCRUserInformation;
38  import org.mycore.user2.annotation.MCRUserAttributeJavaConverter;
39  import org.mycore.user2.utils.MCRRolesConverter;
40  import org.mycore.user2.utils.MCRUserNameConverter;
41  
42  import jakarta.persistence.Access;
43  import jakarta.persistence.AccessType;
44  import jakarta.persistence.CollectionTable;
45  import jakarta.persistence.Column;
46  import jakarta.persistence.ElementCollection;
47  import jakarta.persistence.Entity;
48  import jakarta.persistence.EnumType;
49  import jakarta.persistence.Enumerated;
50  import jakarta.persistence.FetchType;
51  import jakarta.persistence.GeneratedValue;
52  import jakarta.persistence.GenerationType;
53  import jakarta.persistence.Id;
54  import jakarta.persistence.Index;
55  import jakarta.persistence.JoinColumn;
56  import jakarta.persistence.ManyToOne;
57  import jakarta.persistence.NamedQueries;
58  import jakarta.persistence.NamedQuery;
59  import jakarta.persistence.Table;
60  import jakarta.persistence.Transient;
61  import jakarta.persistence.UniqueConstraint;
62  import jakarta.xml.bind.annotation.XmlAccessType;
63  import jakarta.xml.bind.annotation.XmlAccessorType;
64  import jakarta.xml.bind.annotation.XmlAttribute;
65  import jakarta.xml.bind.annotation.XmlElement;
66  import jakarta.xml.bind.annotation.XmlElementWrapper;
67  import jakarta.xml.bind.annotation.XmlRootElement;
68  import jakarta.xml.bind.annotation.XmlType;
69  
70  /**
71   * Represents a login user. Each user has a unique numerical ID.
72   * Each user belongs to a realm. The user name must be unique within a realm.
73   * Any changes made to an instance of this class does not persist automatically.
74   * Use {@link MCRUserManager#updateUser(MCRUser)} to achieve this.
75   *
76   * @author Frank L\u00fctzenkirchen
77   * @author Thomas Scheffler (yagee)
78   * @author Ren\u00E9 Adler (eagle)
79   */
80  @Entity
81  @Access(AccessType.PROPERTY)
82  @Table(name = "MCRUser", uniqueConstraints = @UniqueConstraint(columnNames = { "userName", "realmID" }))
83  @NamedQueries(@NamedQuery(name = "MCRUser.byPropertyValue",
84      query = "SELECT u FROM MCRUser u JOIN FETCH u.attributes ua WHERE ua.name = :name  AND ua.value = :value"))
85  @XmlRootElement(name = "user")
86  @XmlAccessorType(XmlAccessType.NONE)
87  @XmlType(propOrder = { "ownerId", "realName", "eMail", "lastLogin", "validUntil", "roles", "attributes",
88      "password" })
89  public class MCRUser implements MCRUserInformation, Cloneable, Serializable {
90      private static final long serialVersionUID = 3378645055646901800L;
91  
92      /** The unique user ID */
93      int internalID;
94  
95      /** if locked, user may not change this instance */
96      @XmlAttribute(name = "locked")
97      private boolean locked;
98  
99      @XmlAttribute(name = "disabled")
100     private boolean disabled;
101 
102     /** The login user name */
103     @org.mycore.user2.annotation.MCRUserAttribute
104     @MCRUserAttributeJavaConverter(MCRUserNameConverter.class)
105     @XmlAttribute(name = "name")
106     private String userName;
107 
108     @XmlElement
109     private Password password;
110 
111     /** The realm the user comes from */
112     @XmlAttribute(name = "realm")
113     private String realmID;
114 
115     /** The ID of the user that owns this user, or 0 */
116     private MCRUser owner;
117 
118     /** The name of the person that this login user represents */
119     @org.mycore.user2.annotation.MCRUserAttribute
120     @XmlElement
121     private String realName;
122 
123     /** The E-Mail address of the person that this login user represents */
124     @org.mycore.user2.annotation.MCRUserAttribute
125     @XmlElement
126     private String eMail;
127 
128     /** The last time the user logged in */
129     @XmlElement
130     private Date lastLogin;
131 
132     @XmlElement
133     private Date validUntil;
134 
135     private SortedSet<MCRUserAttribute> attributes;
136 
137     @Transient
138     private Collection<String> systemRoles;
139 
140     @Transient
141     private Collection<String> externalRoles;
142 
143     protected MCRUser() {
144         this(null);
145     }
146 
147     /**
148      * Creates a new user.
149      *
150      * @param userName the login user name
151      * @param mcrRealm the realm this user belongs to
152      */
153     public MCRUser(String userName, MCRRealm mcrRealm) {
154         this(userName, mcrRealm.getID());
155     }
156 
157     /**
158      * Creates a new user.
159      *
160      * @param userName the login user name
161      * @param realmID the ID of the realm this user belongs to
162      */
163     public MCRUser(String userName, String realmID) {
164         this.userName = userName;
165         this.realmID = realmID;
166         this.systemRoles = new HashSet<>();
167         this.externalRoles = new HashSet<>();
168         this.attributes = new TreeSet<>();
169         this.password = new Password();
170     }
171 
172     /**
173      * Creates a new user in the default realm.
174      *
175      * @param userName the login user name
176      */
177     public MCRUser(String userName) {
178         this(userName, MCRRealmFactory.getLocalRealm().getID());
179     }
180 
181     /**
182      * @return the internalID
183      */
184     @Id
185     @GeneratedValue(strategy = GenerationType.IDENTITY)
186     @Column
187     int getInternalID() {
188         return internalID;
189     }
190 
191     /**
192      * @param internalID the internalID to set
193      */
194     void setInternalID(int internalID) {
195         this.internalID = internalID;
196     }
197 
198     @Column(name = "locked", nullable = true)
199     public boolean isLocked() {
200         return locked;
201     }
202 
203     public void setLocked(Boolean locked) {
204         this.locked = locked == null ? false : locked;
205     }
206 
207     /**
208      * @return the disabled
209      */
210     @Column(name = "disabled", nullable = true)
211     public boolean isDisabled() {
212         return disabled;
213     }
214 
215     /**
216      * @param disabled the disabled to set
217      */
218     public void setDisabled(Boolean disabled) {
219         this.disabled = disabled == null ? false : disabled;
220     }
221 
222     /**
223      * Returns the login user name. The user name is
224      * unique within its realm.
225      *
226      * @return the login user name.
227      */
228     @Column(name = "userName", nullable = false)
229     public String getUserName() {
230         return userName;
231     }
232 
233     /**
234      * Sets the login user name. The login user name
235      * can be changed as long as it is unique within
236      * its realm and the user ID is not changed.
237      *
238      * @param userName the new login user name
239      */
240     void setUserName(String userName) {
241         this.userName = userName;
242     }
243 
244     /**
245      * Returns the realm the user belongs to.
246      *
247      * @return the realm the user belongs to.
248      */
249     @Transient
250     public MCRRealm getRealm() {
251         return MCRRealmFactory.getRealm(realmID);
252     }
253 
254     /**
255      * Sets the realm this user belongs to.
256      * The realm can be changed as long as the login user name
257      * is unique within the new realm.
258      *
259      * @param realm the realm the user belongs to.
260      */
261     void setRealm(MCRRealm realm) {
262         this.realmID = realm.getID();
263     }
264 
265     /**
266      * Returns the ID of the realm the user belongs to.
267      *
268      * @return the ID of the realm the user belongs to.
269      */
270     @Column(name = "realmID", length = 128, nullable = false)
271     public String getRealmID() {
272         return realmID;
273     }
274 
275     /**
276      * Sets the realm this user belongs to.
277      * The realm can be changed as long as the login user name
278      * is unique within the new realm.
279      *
280      * @param realmID the ID of the realm the user belongs to.
281      */
282     void setRealmID(String realmID) {
283         if (realmID == null) {
284             setRealm(MCRRealmFactory.getLocalRealm());
285         } else {
286             setRealm(MCRRealmFactory.getRealm(realmID));
287         }
288     }
289 
290     /**
291      * @return the hash
292      */
293     @Column(name = "password", nullable = true)
294     public String getPassword() {
295         return password == null ? null : password.hash;
296     }
297 
298     /**
299      * @param password the hash value to set
300      */
301     public void setPassword(String password) {
302         this.password.hash = password;
303     }
304 
305     /**
306      * @return the salt
307      */
308     @Column(name = "salt", nullable = true)
309     public String getSalt() {
310         return password == null ? null : password.salt;
311     }
312 
313     /**
314      * @param salt the salt to set
315      */
316     public void setSalt(String salt) {
317         this.password.salt = salt;
318     }
319 
320     /**
321      * @return the hashType
322      */
323     @Column(name = "hashType", nullable = true)
324     @Enumerated(EnumType.STRING)
325     public MCRPasswordHashType getHashType() {
326         return password == null ? null : password.hashType;
327     }
328 
329     /**
330      * @param hashType the hashType to set
331      */
332     public void setHashType(MCRPasswordHashType hashType) {
333         this.password.hashType = hashType;
334     }
335 
336     /**
337      * Returns the user that owns this user, or null
338      * if the user is independent and has no owner.
339      *
340      * @return the user that owns this user.
341      */
342     @ManyToOne
343     @JoinColumn(name = "owner", nullable = true)
344     public MCRUser getOwner() {
345         return owner;
346     }
347 
348     /**
349      * Sets the user that owns this user.
350      * Setting this to null makes the user independent.
351      *
352      * @param owner the owner of the user.
353      */
354     public void setOwner(MCRUser owner) {
355         this.owner = owner;
356     }
357 
358     /**
359      * Returns true if this user has no owner and therefore
360      * is independent. Independent users may change their passwords
361      * etc., owned users may not, they are created to limit read access
362      * in general.
363      *
364      * @return true if this user has no owner
365      */
366     public boolean hasNoOwner() {
367         return owner == null;
368     }
369 
370     /**
371      * Returns the name of the person this login user represents.
372      *
373      * @return the name of the person this login user represents.
374      */
375     @Column(name = "realName", nullable = true)
376     public String getRealName() {
377         return realName;
378     }
379 
380     /**
381      * Sets the name of the person this login user represents.
382      *
383      * @param realName the name of the person this login user represents.
384      */
385     public void setRealName(String realName) {
386         this.realName = realName;
387     }
388 
389     /**
390      * Returns the E-Mail address of the person this login user represents.
391      *
392      * @return the E-Mail address of the person this login user represents.
393      */
394     @Transient
395     public String getEMailAddress() {
396         return eMail;
397     }
398 
399     @Column(name = "eMail", nullable = true)
400     private String getEMail() {
401         return eMail;
402     }
403 
404     /**
405      * Sets the E-Mail address of the person this user represents.
406      *
407      * @param eMail the E-Mail address
408      */
409     public void setEMail(String eMail) {
410         this.eMail = eMail;
411     }
412 
413     /**
414      * Returns a hint the user has stored in case of forgotten hash.
415      *
416      * @return a hint the user has stored in case of forgotten hash.
417      */
418     @Column(name = "hint", nullable = true)
419     public String getHint() {
420         return password == null ? null : password.hint;
421     }
422 
423     /**
424      * Sets a hint to store in case of hash loss.
425      *
426      * @param hint a hint for the user in case hash is forgotten.
427      */
428     public void setHint(String hint) {
429         this.password.hint = hint;
430     }
431 
432     /**
433      * Returns the last time the user has logged in.
434      *
435      * @return the last time the user has logged in.
436      */
437     @Column(name = "lastLogin", nullable = true)
438     public Date getLastLogin() {
439         if (lastLogin == null) {
440             return null;
441         }
442         return new Date(lastLogin.getTime());
443     }
444 
445     /**
446      * Sets the time of last login.
447      *
448      * @param lastLogin the last time the user logged in.
449      */
450     public void setLastLogin(Date lastLogin) {
451         this.lastLogin = lastLogin == null ? null : new Date(lastLogin.getTime());
452     }
453 
454     /**
455      * Sets the time of last login to now.
456      */
457     public void setLastLogin() {
458         this.lastLogin = new Date();
459     }
460 
461     /* (non-Javadoc)
462      * @see java.lang.Object#equals(java.lang.Object)
463      */
464     @Override
465     public boolean equals(Object obj) {
466         if (this == obj) {
467             return true;
468         }
469         if (obj == null) {
470             return false;
471         }
472         if (!(obj instanceof MCRUser)) {
473             return false;
474         }
475         MCRUser other = (MCRUser) obj;
476         if (realmID == null) {
477             if (other.realmID != null) {
478                 return false;
479             }
480         } else if (!realmID.equals(other.realmID)) {
481             return false;
482         }
483         if (userName == null) {
484             return other.userName == null;
485         } else {
486             return userName.equals(other.userName);
487         }
488     }
489 
490     /* (non-Javadoc)
491      * @see java.lang.Object#hashCode()
492      */
493     @Override
494     public int hashCode() {
495         final int prime = 31;
496         int result = 1;
497         result = prime * result + ((realmID == null) ? 0 : realmID.hashCode());
498         result = prime * result + ((userName == null) ? 0 : userName.hashCode());
499         return result;
500     }
501 
502     @Transient
503     @Override
504     public String getUserID() {
505         String cuid = this.getUserName();
506         if (!getRealm().equals(MCRRealmFactory.getLocalRealm())) {
507             cuid += "@" + getRealmID();
508         }
509 
510         return cuid;
511     }
512 
513     /**
514      * Returns additional user attributes.
515      * This methods handles {@link MCRUserInformation#ATT_REAL_NAME} and
516      * all attributes defined in {@link #getAttributes()}.
517      */
518     @Override
519     public String getUserAttribute(String attribute) {
520         switch (attribute) {
521         case MCRUserInformation.ATT_REAL_NAME:
522             return getRealName();
523         case MCRUserInformation.ATT_EMAIL:
524             return getEMailAddress();
525         default:
526             Set<MCRUserAttribute> attrs = attributes.stream()
527                 .filter(a -> a.getName().equals(attribute))
528                 .collect(Collectors.toSet());
529             if (attrs.size() > 1) {
530                 throw new MCRException(getUserID() + ": user attribute " + attribute + " is not unique");
531             }
532             return attrs.stream()
533                 .map(MCRUserAttribute::getValue)
534                 .findAny().orElse(null);
535         }
536     }
537 
538     @Override
539     public boolean isUserInRole(final String role) {
540         boolean directMember = getSystemRoleIDs().contains(role) || getExternalRoleIDs().contains(role);
541         if (directMember) {
542             return true;
543         }
544         return MCRRoleManager.isAssignedToRole(this, role);
545     }
546 
547     /**
548      * @param attributes the attributes to set
549      */
550     public void setAttributes(SortedSet<MCRUserAttribute> attributes) {
551         this.attributes = attributes;
552     }
553 
554     @ElementCollection(fetch = FetchType.EAGER)
555     @CollectionTable(name = "MCRUserAttr",
556         joinColumns = @JoinColumn(name = "id"),
557         indexes = { @Index(name = "MCRUserAttributes", columnList = "name, value"),
558             @Index(name = "MCRUserValues", columnList = "value") })
559     @SortNatural
560     @XmlElementWrapper(name = "attributes")
561     @XmlElement(name = "attribute")
562     public SortedSet<MCRUserAttribute> getAttributes() {
563         return this.attributes;
564     }
565 
566     /**
567      * Returns a collection any system role ID this user is member of.
568      * @see MCRRole#isSystemRole()
569      */
570     @Transient
571     public Collection<String> getSystemRoleIDs() {
572         return systemRoles;
573     }
574 
575     /**
576      * Returns a collection any external role ID this user is member of.
577      * @see MCRRole#isSystemRole()
578      */
579     @Transient
580     public Collection<String> getExternalRoleIDs() {
581         return externalRoles;
582     }
583 
584     /**
585      * Adds this user to the given role.
586      * @param roleName the role the user should be added to (must already exist)
587      */
588     public void assignRole(String roleName) {
589         MCRRole mcrRole = MCRRoleManager.getRole(roleName);
590         if (mcrRole == null) {
591             throw new MCRException("Could not find role " + roleName);
592         }
593         assignRole(mcrRole);
594     }
595 
596     private void assignRole(MCRRole mcrRole) {
597         if (mcrRole.isSystemRole()) {
598             getSystemRoleIDs().add(mcrRole.getName());
599         } else {
600             getExternalRoleIDs().add(mcrRole.getName());
601         }
602     }
603 
604     /**
605      * Removes this user from the given role.
606      * @param roleName the role the user should be removed from (must already exist)
607      */
608     public void unassignRole(String roleName) {
609         MCRRole mcrRole = MCRRoleManager.getRole(roleName);
610         if (mcrRole == null) {
611             throw new MCRException("Could not find role " + roleName);
612         }
613         if (mcrRole.isSystemRole()) {
614             getSystemRoleIDs().remove(mcrRole.getName());
615         } else {
616             getExternalRoleIDs().remove(mcrRole.getName());
617         }
618     }
619 
620     /**
621      * Enable login for this user.
622      */
623     public void enableLogin() {
624         setDisabled(false);
625     }
626 
627     /**
628      * Disable login for this user.
629      */
630     public void disableLogin() {
631         setDisabled(true);
632     }
633 
634     /**
635      * Returns true if logins are allowed for this user.
636      */
637     public boolean loginAllowed() {
638         return !disabled && (validUntil == null || validUntil.after(new Date()));
639     }
640 
641     /**
642      * Returns a {@link Date} when this user can not login anymore.
643      */
644     @Column(name = "validUntil", nullable = true)
645     public Date getValidUntil() {
646         if (validUntil == null) {
647             return null;
648         }
649         return new Date(validUntil.getTime());
650     }
651 
652     /**
653      * Sets a {@link Date} when this user can not login anymore.
654      * @param validUntil the validUntil to set
655      */
656     public void setValidUntil(Date validUntil) {
657         this.validUntil = validUntil == null ? null : new Date(validUntil.getTime());
658     }
659 
660     //This is used for MCRUserAttributeMapper
661 
662     @Transient
663     Collection<String> getRolesCollection() {
664         return Arrays.stream(getRoles()).map(MCRRole::getName).collect(Collectors.toSet());
665     }
666 
667     @org.mycore.user2.annotation.MCRUserAttribute(name = "roles", separator = ";")
668     @MCRUserAttributeJavaConverter(MCRRolesConverter.class)
669     void setRolesCollection(Collection<String> roles) {
670         for (String role : roles) {
671             assignRole(role);
672         }
673     }
674 
675     //This is code to get JAXB work
676 
677     @Transient
678     @XmlElementWrapper(name = "roles")
679     @XmlElement(name = "role")
680     private MCRRole[] getRoles() {
681         if (getSystemRoleIDs().isEmpty() && getExternalRoleIDs().isEmpty()) {
682             return null;
683         }
684         ArrayList<String> roleIds = new ArrayList<>(getSystemRoleIDs().size() + getExternalRoleIDs().size());
685         Collection<MCRRole> roles = new ArrayList<>(roleIds.size());
686         roleIds.addAll(getSystemRoleIDs());
687         roleIds.addAll(getExternalRoleIDs());
688         for (String roleName : roleIds) {
689             MCRRole role = MCRRoleManager.getRole(roleName);
690             if (role == null) {
691                 throw new MCRException("Could not load role: " + roleName);
692             }
693             roles.add(role);
694         }
695         return roles.toArray(new MCRRole[roles.size()]);
696     }
697 
698     @SuppressWarnings("unused")
699     private void setRoles(MCRRole[] roles) {
700         Stream.of(roles)
701             .map(MCRRole::getName)
702             .map(roleName -> {
703                 //check if role does exist, so we wont lose it on export
704                 MCRRole role = MCRRoleManager.getRole(roleName);
705                 if (role == null) {
706                     throw new MCRException("Could not load role: " + roleName);
707                 }
708                 return role;
709             })
710             .forEach(this::assignRole);
711     }
712 
713     public void setUserAttribute(String name, String value) {
714         Optional<MCRUserAttribute> anyMatch = getAttributes().stream()
715             .filter(a -> a.getName().equals(Objects.requireNonNull(name)))
716             .findAny();
717         if (anyMatch.isPresent()) {
718             MCRUserAttribute attr = anyMatch.get();
719             attr.setValue(value);
720             getAttributes().removeIf(a -> a.getName().equals(name) && a != attr);
721         } else {
722             getAttributes().add(new MCRUserAttribute(name, value));
723         }
724     }
725 
726     @Transient
727     @XmlElement(name = "owner")
728     private UserIdentifier getOwnerId() {
729         if (owner == null) {
730             return null;
731         }
732         UserIdentifier userIdentifier = new UserIdentifier();
733         userIdentifier.name = owner.getUserName();
734         userIdentifier.realm = owner.getRealmID();
735         return userIdentifier;
736     }
737 
738     @SuppressWarnings("unused")
739     private void setOwnerId(UserIdentifier userIdentifier) {
740         if (userIdentifier.name.equals(this.userName) && userIdentifier.realm.equals(this.realmID)) {
741             setOwner(this);
742             return;
743         }
744         MCRUser owner = MCRUserManager.getUser(userIdentifier.name, userIdentifier.realm);
745         setOwner(owner);
746     }
747 
748     /* (non-Javadoc)
749      * @see java.lang.Object#clone()
750      */
751     @Override
752     public MCRUser clone() {
753         MCRUser copy = getSafeCopy();
754         if (copy.password == null) {
755             copy.password = new Password();
756         }
757         copy.password.hashType = this.password.hashType;
758         copy.password.hash = this.password.hash;
759         copy.password.salt = this.password.salt;
760         return copy;
761     }
762 
763     /**
764      * Returns this MCRUser with basic information.
765      * Same as {@link #getSafeCopy()} but without these informations:
766      * <ul>
767      * <li>real name
768      * <li>eMail
769      * <li>attributes
770      * <li>role information
771      * <li>last login
772      * <li>valid until
773      * <li>password hint
774      * </ul>
775      * @return a clone copy of this instance
776      */
777     @Transient
778     public MCRUser getBasicCopy() {
779         MCRUser copy = new MCRUser(userName, realmID);
780         copy.locked = locked;
781         copy.disabled = disabled;
782         copy.owner = this.equals(this.owner) ? copy : this.owner;
783         copy.setAttributes(null);
784         copy.password = null;
785         return copy;
786     }
787 
788     /**
789      * Returns this MCRUser with safe information.
790      * Same as {@link #clone()} but without these informations:
791      * <ul>
792      * <li>password hash type
793      * <li>password hash value
794      * <li>password salt
795      * </ul>
796      * @return a clone copy of this instance
797      */
798     @Transient
799     public MCRUser getSafeCopy() {
800         MCRUser copy = getBasicCopy();
801         if (getHint() != null) {
802             copy.password = new Password();
803             copy.password.hint = getHint();
804         }
805         copy.setAttributes(new TreeSet<>());
806         copy.eMail = this.eMail;
807         copy.lastLogin = this.lastLogin;
808         copy.validUntil = this.validUntil;
809         copy.realName = this.realName;
810         copy.systemRoles.addAll(this.systemRoles);
811         copy.externalRoles.addAll(this.externalRoles);
812         copy.attributes.addAll(this.attributes);
813         return copy;
814     }
815 
816     private static class Password implements Serializable {
817 
818         private static final long serialVersionUID = 8068063832119405080L;
819 
820         @XmlAttribute
821         private String hash;
822 
823         //base64 encoded
824         @XmlAttribute
825         private String salt;
826 
827         @XmlAttribute
828         private MCRPasswordHashType hashType;
829 
830         /** A hint stored by the user in case hash is forgotten */
831         @XmlAttribute
832         private String hint;
833 
834     }
835 
836     private static class UserIdentifier implements Serializable {
837 
838         private static final long serialVersionUID = 4654103884660408929L;
839 
840         @XmlAttribute
841         public String name;
842 
843         @XmlAttribute
844         public String realm;
845     }
846 }