/*******************************************************************************
* Copyright (c) 2015 MEDEVIT.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* MEDEVIT <office@medevit.at> - initial API and implementation
******************************************************************************/
package ch.elexis.data;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.codec.DecoderException;
import ch.elexis.core.jdt.NonNull;
import ch.elexis.core.jdt.Nullable;
import ch.elexis.core.model.RoleConstants;
import ch.rgw.tools.PasswordEncryptionService;
import ch.rgw.tools.StringTool;
public class User extends PersistentObject {
public static final String TABLENAME = "USER_";
public static final String USERNAME_ADMINISTRATOR = "Administrator";
public static final String FLD_IS_ACTIVE = "IS_ACTIVE";
public static final String FLD_IS_ADMINISTRATOR = "IS_ADMINISTRATOR";
public static final String FLD_ASSOC_CONTACT = "KONTAKT_ID";
public static final String FLD_HASHED_PASSWORD = "HASHED_PASSWORD";
public static final String FLD_SALT = "SALT";
public static final String FLD_KEYSTORE = "KEYSTORE";
public static final String FLD_JOINT_ROLES = "Roles";
private static PasswordEncryptionService pes = new PasswordEncryptionService();
static {
addMapping(TABLENAME, FLD_ID, FLD_IS_ACTIVE, FLD_IS_ADMINISTRATOR, FLD_ASSOC_CONTACT,
FLD_HASHED_PASSWORD, FLD_SALT, FLD_KEYSTORE,
FLD_JOINT_ROLES + "=LIST:USER_ID:USER_ROLE_JOINT");
initTables();
}
protected static void initTables() {
if (!tableExists(TABLENAME)) {
executeDBInitScriptForClass(User.class, null);
User.migrateToNewStructure();
}
}
public User(){}
/**
* Every new {@link User} is assigned the {@link Role#SYSTEMROLE_LITERAL_USER}
*
* @param anw
* @param username
* @param password
*/
public User(Anwender anw, String username, String password){
create(username);
setAssignedContact(anw);
if (password == null || password.length() == 0) {
password = StringTool.unique("pswd");
}
setPassword(password);
setAssignedRole(Role.load(RoleConstants.SYSTEMROLE_LITERAL_USER), true);
}
protected User(final String id){
super(id);
}
/**
* Generates a user object, which is not necessarily backed with a real db
* object, use {@link #exists()} to check.
*
* @param id
* @return
*/
public static @NonNull User load(final String id) {
return new User(id);
}
/**
* Transfer existing users into the new separated table.<br>
* Every {@link Anwender} is automatically assigned to the role {@link Role#SYSTEMROLE_LITERAL_USER}.
* Every {@link Mandant} is additionally assigned to the role
* {@link Role#SYSTEMROLE_LITERAL_EXECUTIVE_DOCTOR}.
*
* @see https://redmine.medelexis.ch/issues/771
*/
private static void migrateToNewStructure(){
Role.initTables();
log.info("Starting migration to new user structure");
Query<Anwender> qbe = new Query<Anwender>(Anwender.class);
List<Anwender> users = qbe.execute();
for (Anwender anwender : users) {
String username = anwender.get(Kontakt.FLD_NAME3);
if(username==null || username.length()==0) {
log.warn("Username for Anwender "+anwender.getLabel()+" not set. Skipping user creation.");
continue;
}
String password = (String) anwender.getExtInfoStoredObjectByKey("UsrPwd");
boolean setActive=true;
if(password==null || password.length()==0) {
password = "pass";
log.warn("Password for Anwender "+anwender.getLabel()+" is empty, setting 'pass' and deactivating user.");
setActive = false;
}
User u;
if (username.equals(USERNAME_ADMINISTRATOR)) {
u = User.load(USERNAME_ADMINISTRATOR);
u.setAssignedContact(anwender);
u.setPassword(password);
log.info("Overriding Administrator password with password from anwender [{}]",
anwender.getLabel());
} else {
u = new User(anwender, username, password);
}
u.setActive(setActive);
boolean isMandator = anwender.getBoolean(Anwender.FLD_IS_MANDATOR);
if (isMandator) {
u.setAssignedRole(Role.load(RoleConstants.SYSTEMROLE_LITERAL_EXECUTIVE_DOCTOR), true);
u.setAssignedRole(Role.load(RoleConstants.SYSTEMROLE_LITERAL_DOCTOR), true);
}
log.info("Migrated anwender [{}] to new user structure with id [{}]",
anwender.getLabel(), u.getId());
// TODO delete the information from contact table?
}
}
/**
*
* @return
* @since 3.1
*/
public List<Role> getAssignedRoles(){
List<String> roles = getList(FLD_JOINT_ROLES, false);
return roles.stream().map(p -> Role.load(p)).collect(Collectors.toList());
}
/**
*
* @param role
* @since 3.1
*/
public void setAssignedRole(Role role, boolean isAssigned){
List<Role> assignedRoles = getAssignedRoles();
if (isAssigned) {
if (assignedRoles.contains(role))
return;
addToList(FLD_JOINT_ROLES, role.getId());
} else {
if (!assignedRoles.contains(role))
return;
removeFromList(FLD_JOINT_ROLES, role.getId());
}
}
/**
*
* @param attemptedPassword
* @return
*/
public boolean verifyPassword(String attemptedPassword){
boolean ret = false;
String[] values = get(false, FLD_HASHED_PASSWORD, FLD_SALT);
try {
ret = pes.authenticate(attemptedPassword, values[0], values[1]);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | DecoderException e) {
log.error("Error verifying password", e);
}
return ret;
}
public String getUsername(){
return get(FLD_ID);
}
/**
* verify whether the proposed username is not already in use
*
* @param username
* @return <code>true</code> if the given username may be used
*/
public static boolean verifyUsernameNotTaken(String username){
Query<User> qbe = new Query<User>(User.class);
qbe.clear(true);
qbe.add(User.FLD_ID, Query.EQUALS, username);
return qbe.execute().size() == 0;
}
/**
* set the new username, where the username is equivalent to the ID, invalidates the given
* object returning the new one
*
* @param username
* @return the new {@link User} object as a result of the rename
*/
public @Nullable User setUsername(String username){
if (verifyUsernameNotTaken(username)) {
// we have to re-target the assigned roles to the new username
List<Role> assignedRoles = getAssignedRoles();
assignedRoles.stream().forEachOrdered(r -> setAssignedRole(r, false));
set(FLD_ID, username);
User u = User.load(username);
assignedRoles.stream().forEachOrdered(r -> u.setAssignedRole(r, true));
return u;
}
return null;
}
/**
* set the password
* @param pwd
*/
public void setPassword(@NonNull final String pwd){
try {
String salt = pes.generateSaltAsHexString();
String hashed_pw = pes.getEncryptedPasswordAsHexString(pwd, salt);
set(new String[] {
FLD_SALT, FLD_HASHED_PASSWORD
}, salt, hashed_pw);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | DecoderException e) {
log.error("Error setting password for contact", e);
}
}
public void setAssignedContact(@Nullable Kontakt contact){
if (contact == null)
return;
set(FLD_ASSOC_CONTACT, contact.getId());
}
/**
* @return contact, castable to {@link Anwender}
*/
public @Nullable String getAssignedContactId(){
return get(FLD_ASSOC_CONTACT);
}
public boolean isAdministrator(){
return getBoolean(FLD_IS_ADMINISTRATOR);
}
public void setAdministrator(boolean val){
set(FLD_IS_ADMINISTRATOR, ts(val));
}
@Override
public String getLabel(){
return getUsername();
}
@Override
protected String getTableName(){
return TABLENAME;
}
public @Nullable Anwender getAssignedContact(){
String assocId = getAssignedContactId();
if (assocId != null && assocId.length() > 1) {
return Anwender.load(assocId);
}
return null;
}
public boolean isActive(){
return getBoolean(FLD_IS_ACTIVE);
}
public void setActive(boolean val){
set(FLD_IS_ACTIVE, ts(val));
}
@Override
public boolean delete(){
getAssignedRoles().stream().forEachOrdered(r -> setAssignedRole(r, false));
return super.delete();
}
}