package uk.ac.ebi.fg.myequivalents.access_control.model; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Lob; import javax.persistence.Table; import javax.persistence.Transient; import javax.xml.bind.DatatypeConverter; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import org.apache.commons.lang.StringUtils; import org.hibernate.annotations.Index; import uk.ac.ebi.fg.myequivalents.resources.Const; import uk.ac.ebi.fg.myequivalents.utils.jaxb.PasswordJaxbXmlAdapter; /** * A system user. This is used to manage a minimum form of access control and provenance. * * <dl><dt>date</dt><dd>Mar 1, 2013</dd></dl> * @author Marco Brandizi * */ @Entity @Table( name = "acc_ctrl_user" ) @XmlRootElement ( name = "user" ) @XmlAccessorType ( XmlAccessType.NONE ) public class User { public static enum Role { VIEWER ( 0 ), EDITOR( 100 ), ADMIN ( 1000 ); /** * The higher the level, the more rights you have. */ private final int level; private Role ( int level ) { this.level = level; } /** * It tells you if this role is is equal to or higher than the one passed as parameter. */ public boolean hasPowerOf ( Role role ) { return this.level >= role.level; } } private String name; private String surname; private String email; private String password; private String notes; private Role role; private String apiPassword; protected User () { super (); } public User ( String email ) { super (); email = StringUtils.trimToNull ( email ); if ( email == null ) // TODO: proper exception throw new NullPointerException ( "Name cannot be empty" ); this.setEmail ( email ); } public User ( String email, String name, String surname, String password, String notes, Role role, String apiPassword ) { super (); this.name = name; this.surname = surname; this.email = email; this.password = password; this.notes = notes; this.role = role; this.apiPassword = apiPassword; } public User ( User original ) { this ( original.getEmail (), original.getName (), original.getSurname (), original.getPassword (), original.getNotes (), original.getRole (), original.getApiPassword () ); } @Index ( name = "user_n" ) @Column ( length = Const.COL_LENGTH_S ) @XmlAttribute ( name = "name" ) public String getName () { return name; } public void setName ( String name ) { this.name = name; } @Index ( name = "user_s" ) @Column ( length = Const.COL_LENGTH_S ) @XmlAttribute ( name = "surname" ) public String getSurname () { return surname; } public void setSurname ( String surname ) { this.surname = surname; } @Id @Column ( length = Const.COL_LENGTH_S ) @XmlAttribute ( name = "email" ) public String getEmail () { return email; } protected void setEmail ( String email ) { this.email = email; } /** * The SHA1+Base64 encrypted version of the password for this user. This is used for user management operations, * {@link #getApiPassword()} is used for API invocations. The hashed password is expected to have the same format * that {@link #hashPassword(String)} generates. * */ @Column ( name = "password", columnDefinition = "char(27)", nullable = false ) @XmlAttribute ( name = "password" ) @XmlJavaTypeAdapter ( PasswordJaxbXmlAdapter.class ) public String getPassword () { return password; } public void setPassword ( String password ) { this.password = password; } @Lob @XmlElement ( name = "notes" ) public String getNotes () { return notes; } public void setNotes ( String notes ) { this.notes = notes; } @Transient public Role getRole () { return role; } public void setRole ( Role role ) { this.role = role; } @Column ( name = "role", nullable = true, length = Const.COL_LENGTH_S ) // if null, no credential @XmlAttribute ( name = "role" ) protected String getRoleAsString () { return this.role == null ? null : this.role.toString (); } protected void setRoleAsString ( String role ) { this.role = role == null ? null : Role.valueOf ( role ); } /** * A SHA1+Base64 password to be used for API invocations. {@link #getPassword()} is used for user management operations. * The hashed password is expected to have the same format that {@link #hashPassword(String)} generates. * */ @Column ( name = "secret", columnDefinition = "char(27)", nullable = false ) @XmlAttribute ( name = "secret" ) @XmlJavaTypeAdapter ( PasswordJaxbXmlAdapter.class ) public String getApiPassword () { return apiPassword; } public void setApiPassword ( String apiPassword ) { this.apiPassword = apiPassword; } public boolean hasPowerOf ( Role role ) { return this.getRole ().hasPowerOf ( role ); } public boolean equals ( Object o ) { if ( o == null ) return false; if ( this == o ) return true; if ( this.getClass () != o.getClass () ) return false; User that = (User) o; return this.getEmail ().equals ( that.getEmail () ); } public int hashCode () { return this.getEmail ().hashCode (); } @Override public String toString () { return String.format ( "User { email: '%s', name: '%s', surname: '%s', role: %s, notes: '%.15s'", this.getEmail (), this.getName (), this.getSurname (), this.getRole ().toString (), this.getNotes () ); } /** * Takes a clear password and converts it into a SHA1 code, then it encodes in a format suitable to be stored into a * relational database and manipulated through SQL statements. Currently we use Base64 */ public static String hashPassword ( String clearPassword ) { try { clearPassword = StringUtils.trimToNull ( clearPassword ); if ( clearPassword == null ) throw new IllegalArgumentException ( "Cannot accept null password" ); MessageDigest messageDigest = MessageDigest.getInstance ( "SHA1" ); // With 20 bytes as input, the BASE64 encoding is always a 27 character string, with the last character always equals // a padding '=', so we don't need the latter in this context return DatatypeConverter.printBase64Binary ( messageDigest.digest ( clearPassword.getBytes () ) ).substring (0, 27); } catch ( NoSuchAlgorithmException ex ) { throw new RuntimeException ( "Internal error, cannot get the SHA1 digester from the JVM", ex ); } } /** * Generates a random password of 32 bytes and converts it into Base64. This is used for auto-generation of e.g., * API passwords. */ public static String generateSecret () { SecureRandom randomGen = new SecureRandom (); byte secret[] = new byte [ 32 ]; randomGen.nextBytes ( secret ); // Last char is always a padding '=' and we don't need it here return DatatypeConverter.printBase64Binary ( secret ).substring ( 0, 43 ); } }