Commit 08e81a56 authored by Marco Descher's avatar Marco Descher

[9918] Refactor login process, provide login contributors

parent 9d919e13
Pipeline #12481 passed with stages
in 4 minutes and 58 seconds
......@@ -142,7 +142,7 @@ public class Desk implements IApplication {
context.applicationRunning();
// perform login
cod.performLogin(UiDesk.getDisplay().getActiveShell());
cod.performLogin(new Shell(UiDesk.getDisplay()));
if ((CoreHub.getLoggedInContact() == null) || !CoreHub.getLoggedInContact().isValid()) {
// no valid user, exit (don't consider this as an error)
log.warn("Exit because no valid user logged-in"); //$NON-NLS-1$
......
......@@ -14,7 +14,6 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
......@@ -22,8 +21,6 @@ import java.util.Properties;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.security.auth.login.LoginException;
import org.eclipse.equinox.internal.app.CommandLineArgs;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
......@@ -44,14 +41,12 @@ import ch.elexis.core.data.events.ElexisEventDispatcher;
import ch.elexis.core.data.events.Heartbeat;
import ch.elexis.core.data.events.Heartbeat.HeartListener;
import ch.elexis.core.data.events.PatientEventListener;
import ch.elexis.core.data.extension.CoreOperationAdvisorHolder;
import ch.elexis.core.data.interfaces.ShutdownJob;
import ch.elexis.core.data.interfaces.events.MessageEvent;
import ch.elexis.core.data.interfaces.scripting.Interpreter;
import ch.elexis.core.data.preferences.CorePreferenceInitializer;
import ch.elexis.core.data.server.ElexisServerEventService;
import ch.elexis.core.data.service.ContextServiceHolder;
import ch.elexis.core.data.service.CoreModelServiceHolder;
import ch.elexis.core.data.service.LocalLockServiceHolder;
import ch.elexis.core.data.service.PoOrderService;
import ch.elexis.core.data.service.StockCommissioningSystemService;
......@@ -64,15 +59,12 @@ import ch.elexis.core.jdt.Nullable;
import ch.elexis.core.model.IContact;
import ch.elexis.core.model.IUser;
import ch.elexis.core.services.IContextService;
import ch.elexis.core.services.IExternalLoginService;
import ch.elexis.core.utils.OsgiServiceUtil;
import ch.elexis.data.Anwender;
import ch.elexis.data.Kontakt;
import ch.elexis.data.Mandant;
import ch.elexis.data.PersistentObject;
import ch.elexis.data.PersistentObjectFactory;
import ch.elexis.data.Query;
import ch.elexis.data.User;
import ch.rgw.io.LockFile;
import ch.rgw.io.Settings;
import ch.rgw.io.SqlSettings;
......@@ -494,83 +486,11 @@ public class CoreHub implements BundleActivator {
}
/**
* Login: Anwender anmelden, passenden Mandanten anmelden. (Jeder Anwender ist entweder selber
* ein Mandant oder ist einem Mandanten zugeordnet)
*
* @param username
* Kurzname
* @param password
* Passwort
* @return <code>true</code> erfolgreich angemeldet, {@link CoreHub#getLoggedInContact()}
* gesetzt, else <code>false</code>
* @since 3.1 queries {@link User}
* @since 3.8 moved from Anwender to CoreHub
* @since 3.8
*/
public static boolean login(final String username, char[] password){
public static void reconfigureServices() {
((LocalLockService) LocalLockServiceHolder.get()).reconfigure();
((ElexisServerEventService) CoreHub.getElexisServerEventService()).reconfigure();
CoreHub.logoffAnwender();
Optional<IExternalLoginService> externalLoginService =
OsgiServiceUtil.getService(IExternalLoginService.class);
IUser user = null;
if (externalLoginService.isPresent()) {
try {
user = externalLoginService.get().login(username, password);
} catch (LoginException e) {
log.info("Login to external system failed", e);
}
}
if (user == null) {
// fallback internal login
log.info("login to internal db");
Optional<IUser> dbUser = CoreModelServiceHolder.get().load(username, IUser.class);
if (dbUser.isPresent()) {
user = dbUser.get().login(username, password);
}
if (user == null) {
return false;
}
}
// check anwender is valid
Anwender anwender = Anwender.load(user.getAssignedContact().getId());
if (anwender == null) {
log.error("username: {}", username, new LoginException("anwender is null"));
return false;
}
if (!anwender.isValid()) {
log.error("username: {}", username,
new LoginException("anwender is invalid or deleted"));
return false;
}
if (!anwender.istAnwender()) {
log.error("username: {}", username,
new LoginException("anwender is not a istAnwender"));
return false;
}
//security - reset password in memory
Arrays.fill(password, '*');
// set user in system
ContextServiceHolder.get().setActiveUser(user);
ElexisEventDispatcher.getInstance()
.fire(new ElexisEvent(CoreHub.getLoggedInContact(), Anwender.class, ElexisEvent.EVENT_USER_CHANGED));
CoreOperationAdvisorHolder.get().adaptForUser();
CoreHub.getLoggedInContact().setInitialMandator();
CoreHub.userCfg = getUserSetting(CoreHub.getLoggedInContact());
CoreHub.heart.resume(true);
return true;
}
/**
......
......@@ -12,6 +12,7 @@ package ch.elexis.core.data.extension;
import ch.elexis.core.data.activator.CoreHub;
import ch.elexis.core.data.util.IRunnableWithProgress;
import ch.elexis.core.jdt.Nullable;
import ch.elexis.data.Anwender;
import ch.elexis.data.PersistentObject;
......@@ -73,14 +74,15 @@ public interface ICoreOperationAdvisor {
* Required Post-Condition: {@link CoreHub#actUser} and {@link CoreHub#actMandant} have to
* contain valid elements.
*
* UI-useage: Presents either the user a dialog prompting for username/password or uses the
* UI-usage: Presents either the user a dialog prompting for username/password or uses the
* System.properties ch.elexis.username and ch.elexis.password to bypass the login dialog. The
* second is needed for automated GUI tests.
*
* @param shell
* and object castable to org.eclipse.swt.widgets.Shell
* an object castable to org.eclipse.swt.widgets.Shell or <code>null</code>
* @return if the login was successful
*/
public void performLogin(Object shell);
public boolean performLogin(@Nullable Object shell);
/**
* UI only
......
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="ch.elexis.core.services.internal.EnvVarsLoginContributor">
<service>
<provide interface="ch.elexis.core.services.ILoginContributor"/>
</service>
<implementation class="ch.elexis.core.services.internal.EnvVarsLoginContributor"/>
</scr:component>
\ No newline at end of file
package ch.elexis.core.services.internal;
import java.util.Optional;
import javax.security.auth.login.LoginException;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Component;
import org.slf4j.LoggerFactory;
import ch.elexis.core.constants.ElexisSystemPropertyConstants;
import ch.elexis.core.model.IUser;
import ch.elexis.core.services.ILoginContributor;
import ch.elexis.core.services.holder.CoreModelServiceHolder;
/**
* Allow bypassing the login dialog, eg. for automated GUI-tests. Example: when having a demoDB you
* may login directly by passing
* <code>-vmargs -Dch.elexis.username=test -Dch.elexis.password=test</code> as command line
* parameters to Elexis.
*
* This service performs authentication only against the local database.
*
* @since 3.8 extracted from CoreOperationAdvisor
*/
@Component(immediate = true)
public class EnvVarsLoginContributor implements ILoginContributor {
@Override
public int getPriority(){
return 1000;
}
@Override
public IUser performLogin(Object shell) throws LoginException{
String username = System.getProperty(ElexisSystemPropertyConstants.LOGIN_USERNAME);
String password = System.getProperty(ElexisSystemPropertyConstants.LOGIN_PASSWORD);
if (StringUtils.isNotEmpty(username)) {
LoggerFactory.getLogger(getClass())
.warn("Bypassing LoginDialog with username " + username);
Optional<IUser> dbUser = CoreModelServiceHolder.get().load(username, IUser.class);
if (dbUser.isPresent()) {
IUser user = dbUser.get().login(username, password.toCharArray());
if (user != null && user.isActive()) {
return user;
} else {
LoggerFactory.getLogger(getClass()).error("Authentication failed.");
}
}
}
return null;
}
}
......@@ -94,4 +94,5 @@ Service-Component: OSGI-INF/ch.elexis.core.ui.services.LocalDocumentServiceHolde
OSGI-INF/ch.elexis.core.ui.services.internal.ContextService.xml,
OSGI-INF/ch.elexis.core.ui.util.CoreUiUtil.xml,
OSGI-INF/ch.elexis.core.ui.services.EncounterServiceHolder.xml,
OSGI-INF/ch.elexis.core.ui.internal.CoreOperationAdvisor.xml
OSGI-INF/ch.elexis.core.ui.internal.CoreOperationAdvisor.xml,
OSGI-INF/ch.elexis.core.ui.services.internal.LoginDialogLoginContributor.xml
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="ch.elexis.core.ui.internal.CoreOperationAdvisor">
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="ch.elexis.core.ui.internal.CoreOperationAdvisor">
<service>
<provide interface="ch.elexis.core.data.extension.ICoreOperationAdvisor"/>
</service>
<reference cardinality="1..n" field="loginServices" interface="ch.elexis.core.services.ILoginContributor" name="loginServices"/>
<implementation class="ch.elexis.core.ui.internal.CoreOperationAdvisor"/>
</scr:component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="ch.elexis.core.ui.services.internal.LoginDialogLoginContributor">
<service>
<provide interface="ch.elexis.core.services.ILoginContributor"/>
</service>
<implementation class="ch.elexis.core.ui.services.internal.LoginDialogLoginContributor"/>
</scr:component>
\ No newline at end of file
......@@ -90,7 +90,6 @@ import ch.elexis.core.ui.constants.ExtensionPointConstantsUi;
import ch.elexis.core.ui.constants.UiPreferenceConstants;
import ch.elexis.core.ui.constants.UiResourceConstants;
import ch.elexis.core.ui.dialogs.DateSelectorDialog;
import ch.elexis.core.ui.dialogs.LoginDialog;
import ch.elexis.core.ui.dialogs.NeuerFallDialog;
import ch.elexis.core.ui.dialogs.SelectFallDialog;
import ch.elexis.core.ui.icons.Images;
......@@ -329,22 +328,15 @@ public class GlobalActions {
w.close();
}
}
CoreHub.logoffAnwender();
LoginDialog dlg = new LoginDialog(win.getShell());
dlg.create();
dlg.setTitle(Messages.GlobalActions_LoginDialogTitle); //$NON-NLS-1$
dlg.setMessage(Messages.GlobalActions_LoginDialogMessage); //$NON-NLS-1$
// dlg.getButton(IDialogConstants.CANCEL_ID).setText("Beenden");
dlg.getShell().setText(Messages.GlobalActions_LoginDialogShelltext); //$NON-NLS-1$
if (dlg.open() == Dialog.CANCEL) {
boolean performLogin =
CoreOperationAdvisorHolder.get().performLogin(win.getShell());
if (!performLogin) {
exitAction.run();
}
CoreOperationAdvisorHolder.get().adaptForUser();
} catch (Exception ex) {
ExHandler.handle(ex);
}
System.out.println("login"); //$NON-NLS-1$
}
};
importAction = new RestrictedAction(AC_IMORT, Messages.GlobalActions_Import) { //$NON-NLS-1$
......
......@@ -11,6 +11,10 @@
package ch.elexis.core.ui.internal;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import java.util.List;
import javax.security.auth.login.LoginException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
......@@ -20,28 +24,37 @@ import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.elexis.core.constants.Preferences;
import ch.elexis.core.data.activator.CoreHub;
import ch.elexis.core.data.constants.ElexisSystemPropertyConstants;
import ch.elexis.core.data.events.ElexisEvent;
import ch.elexis.core.data.events.ElexisEventDispatcher;
import ch.elexis.core.data.extension.CoreOperationAdvisorHolder;
import ch.elexis.core.data.extension.ICoreOperationAdvisor;
import ch.elexis.core.data.service.ContextServiceHolder;
import ch.elexis.core.data.util.IRunnableWithProgress;
import ch.elexis.core.ui.Messages;
import ch.elexis.core.model.IUser;
import ch.elexis.core.services.ILoginContributor;
import ch.elexis.core.ui.UiDesk;
import ch.elexis.core.ui.actions.GlobalActions;
import ch.elexis.core.ui.constants.UiResourceConstants;
import ch.elexis.core.ui.dialogs.ErsterMandantDialog;
import ch.elexis.core.ui.dialogs.LoginDialog;
import ch.elexis.core.ui.util.SWTHelper;
import ch.elexis.core.ui.util.SqlWithUiRunner;
import ch.elexis.core.ui.wizards.DBConnectWizard;
import ch.elexis.core.utils.CoreUtil;
import ch.elexis.data.Anwender;
@Component
public class CoreOperationAdvisor implements ICoreOperationAdvisor {
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE)
private List<ILoginContributor> loginServices;
public String initialPerspectiveString;
private Logger log = LoggerFactory.getLogger(CoreOperationAdvisor.class);
......@@ -142,28 +155,41 @@ public class CoreOperationAdvisor implements ICoreOperationAdvisor {
}
@Override
public void performLogin(Object shell){
String username = System.getProperty(ElexisSystemPropertyConstants.LOGIN_USERNAME);
String password = System.getProperty(ElexisSystemPropertyConstants.LOGIN_PASSWORD);
if (username != null && password != null) {
/*
* Allow bypassing the login dialog, eg. for automated GUI-tests. Example: when having a
* demoDB you may login directly by passing -vmargs -Dch.elexis.username=test
* -Dch.elexis.password=test as command line parameters to elexis.
*/
log.error("Bypassing LoginDialog with username " + username);
if (!CoreHub.login(username, password.toCharArray())) {
log.error("Authentication failed. Exiting");
public boolean performLogin(Object shell){
CoreHub.reconfigureServices();
CoreHub.logoffAnwender();
loginServices.sort(Comparator.comparing(ILoginContributor::getPriority));
IUser user = null;
for (ILoginContributor loginService : loginServices) {
try {
user = loginService.performLogin(shell);
if (user != null) {
break;
}
} catch (LoginException le) {
log.warn("Unable to login with loginService [{}]: {} - skipping",
loginService.getClass().getName(), le.getMessage(), le);
}
} else {
LoginDialog dlg = new LoginDialog((Shell) shell);
dlg.create();
dlg.getShell().setText(Messages.LoginDialog_loginHeader);
dlg.setTitle(Messages.LoginDialog_notLoggedIn);
dlg.setMessage(Messages.LoginDialog_enterUsernamePass);
dlg.open();
// TODO adaptForUser
}
if (user != null && user.isActive()) {
// set user in system
ContextServiceHolder.get().setActiveUser(user);
ElexisEventDispatcher.getInstance().fire(new ElexisEvent(CoreHub.getLoggedInContact(),
Anwender.class, ElexisEvent.EVENT_USER_CHANGED));
CoreOperationAdvisorHolder.get().adaptForUser();
CoreHub.getLoggedInContact().setInitialMandator();
CoreHub.userCfg = CoreHub.getUserSetting(CoreHub.getLoggedInContact());
CoreHub.heart.resume(true);
return true;
}
return false;
}
@Override
......
/*******************************************************************************
* Copyright (c) 2005-2011, G. Weirich and Elexis
* Copyright (c) 2005-2019, G. Weirich and Elexis
* 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
......@@ -11,26 +11,31 @@
*
*******************************************************************************/
package ch.elexis.core.ui.dialogs;
package ch.elexis.core.ui.services.internal;
import java.util.List;
import java.util.Optional;
import javax.security.auth.login.LoginException;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.slf4j.LoggerFactory;
import ch.elexis.core.data.activator.CoreHub;
import ch.elexis.core.data.service.ContextServiceHolder;
import ch.elexis.core.data.service.CoreModelServiceHolder;
import ch.elexis.core.data.util.Extensions;
import ch.elexis.core.l10n.Messages;
import ch.elexis.core.model.IUser;
import ch.elexis.core.ui.ILoginNews;
import ch.elexis.core.ui.constants.ExtensionPointConstantsUi;
import ch.elexis.core.ui.util.SWTHelper;
......@@ -38,12 +43,13 @@ import ch.elexis.data.Anwender;
import ch.elexis.data.Query;
import ch.rgw.tools.ExHandler;
public class LoginDialog extends TitleAreaDialog {
Text usr, pwd;
boolean hasUsers;
ButtonEnabler be = new ButtonEnabler();
public class LocalUserLoginDialog extends TitleAreaDialog {
private Text usr, pwd;
private boolean hasUsers;
private IUser user;
public LoginDialog(Shell parentShell){
public LocalUserLoginDialog(Shell parentShell){
super(parentShell);
Query<Anwender> qbe = new Query<Anwender>(Anwender.class);
......@@ -96,16 +102,44 @@ public class LoginDialog extends TitleAreaDialog {
@Override
protected void okPressed(){
if (CoreHub.login(usr.getText(), pwd.getTextChars()) == true) {
super.okPressed();
} else {
setMessage(Messages.LoginDialog_4, IMessageProvider.ERROR);
// getButton(IDialogConstants.OK_ID).setEnabled(false);
// load by local database
String username = usr.getText();
IUser _user = null;
Optional<IUser> dbUser = CoreModelServiceHolder.get().load(username, IUser.class);
if (dbUser.isPresent()) {
_user = dbUser.get().login(username, pwd.getTextChars());
}
if (_user != null && _user.isActive()) {
Anwender anwender = Anwender.load(_user.getAssignedContact().getId());
if (anwender != null) {
if (anwender.isValid()) {
if (anwender.istAnwender()) {
user = _user;
super.okPressed();
return;
} else {
LoggerFactory.getLogger(getClass()).error("username: {}", username,
new LoginException("anwender is not a istAnwender"));
}
} else {
LoggerFactory.getLogger(getClass()).error("username: {}", username,
new LoginException("anwender is invalid or deleted"));
}
} else {
LoggerFactory.getLogger(getClass()).error("username: {}", username,
new LoginException("anwender is null"));
}
}
setMessage(Messages.LoginDialog_4, IMessageProvider.ERROR);
}
@Override
protected void cancelPressed(){
// GlobalActions.exitAction?
ContextServiceHolder.get().setActiveUser(null);
CoreHub.actMandant = null;
super.cancelPressed();
......@@ -120,18 +154,8 @@ public class LoginDialog extends TitleAreaDialog {
}
class ButtonEnabler implements ModifyListener {
@Override
public void modifyText(ModifyEvent e){
if (usr.getText().length() == 0 || pwd.getText().length() == 0) {
// getButton(IDialogConstants.OK_ID).setEnabled(false);
} else {
// getButton(IDialogConstants.OK_ID).setEnabled(true);
}
}
public IUser getUser(){
return user;
}
}
package ch.elexis.core.ui.services.internal;
import javax.security.auth.login.LoginException;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.swt.widgets.Shell;
import org.osgi.service.component.annotations.Component;
import ch.elexis.core.l10n.Messages;
import ch.elexis.core.model.IUser;
import ch.elexis.core.services.ILoginContributor;
@Component(immediate = true)
public class LoginDialogLoginContributor implements ILoginContributor {
@Override
public int getPriority(){
return 0;
}
@Override
public IUser performLogin(Object shell) throws LoginException{
if (shell instanceof Shell) {
LocalUserLoginDialog loginDialog = new LocalUserLoginDialog((Shell) shell);
loginDialog.create();
loginDialog.getShell().setText(Messages.LoginDialog_loginHeader);
loginDialog.setTitle(Messages.LoginDialog_notLoggedIn);
loginDialog.setMessage(Messages.LoginDialog_enterUsernamePass);
int retval = loginDialog.open();
if (retval == Dialog.OK) {
return loginDialog.getUser();
}
}
return null;
}
}
package ch.elexis.core.services;
import javax.security.auth.login.LoginException;
import ch.elexis.core.model.IUser;
/**
* Contribute a login method. Methods are consulted ordered by {@link #getPriority()}. If
* {@link #performLogin(Object)} returns an {@link IUser} no further login contributors are
* consulted.
*
* @since 3.8
*/
public interface ILoginContributor {
/**
*
* @return the priority - higher precedence in login process
*/
public int getPriority();
/**
* Try to login a user. The respective login tokens to be queried, and the way to query them are
* subject to the service. The service has to take care that {@link IUser#getAssignedContact()}
* is valid - if this is not the case, it MUST return <code>null</code>
*
* @return an {@link IUser} object if login was successful, else <code>null</code>
* @param shell
* an object castable to <code>org.eclipse.swt.widgets.Shell</code> or
* <code>null</code>
* @throws LoginException
* if there was an error in employing this login-service - that is, login success
* could not be determined due to a certain reason
*/
public IUser performLogin(Object shell) throws LoginException;
}
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="ch.elexis.data.service.internal.TestOnlyCoreOperationAdvisor">
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="ch.elexis.data.service.internal.TestOnlyCoreOperationAdvisor">
<service>
<provide interface="ch.elexis.core.data.extension.ICoreOperationAdvisor"/>
</service>
<reference cardinality="1..n" field="loginServices" interface="ch.elexis.core.services.ILoginContributor" name="loginServices"/>
<implementation class="ch.elexis.data.service.internal.TestOnlyCoreOperationAdvisor"/>
</scr:component>
\ No newline at end of file
......@@ -13,6 +13,7 @@ import org.junit.Ignore;
import org.junit.Test;
import ch.elexis.core.data.activator.CoreHub;
import ch.elexis.core.data.extension.CoreOperationAdvisorHolder;
import ch.elexis.core.data.service.ContextServiceHolder;
import ch.elexis.core.model.RoleConstants;