package org.openslx.dozmod.gui.window;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.Iterator;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
import org.apache.thrift.TBaseHelper;
import org.apache.thrift.TException;
import org.openslx.bwlp.thrift.iface.Organization;
import org.openslx.bwlp.thrift.iface.Satellite;
import org.openslx.dozmod.App;
import org.openslx.dozmod.Config;
import org.openslx.dozmod.authentication.Authenticator;
import org.openslx.dozmod.authentication.Authenticator.AuthenticationData;
import org.openslx.dozmod.authentication.Authenticator.AuthenticatorCallback;
import org.openslx.dozmod.authentication.EcpAuthenticator;
import org.openslx.dozmod.authentication.FingerprintManager;
import org.openslx.dozmod.authentication.ShibbolethEcp.ReturnCode;
import org.openslx.dozmod.authentication.TestAccountAuthenticator;
import org.openslx.dozmod.gui.Gui;
import org.openslx.dozmod.gui.MainWindow;
import org.openslx.dozmod.gui.helper.MessageType;
import org.openslx.dozmod.gui.helper.TextChangeListener;
import org.openslx.dozmod.gui.window.layout.LoginWindowLayout;
import org.openslx.dozmod.thrift.Session;
import org.openslx.dozmod.thrift.ThriftActions;
import org.openslx.dozmod.thrift.ThriftError;
import org.openslx.dozmod.thrift.cache.OrganizationCache;
import org.openslx.dozmod.util.DesktopEnvironment;
import org.openslx.dozmod.util.DesktopEnvironment.Link;
import org.openslx.util.QuickTimer;
import org.openslx.util.QuickTimer.Task;
import edu.kit.scc.dei.ecplean.ECPAuthenticationException;
/**
* @author Jonathan Bauer
*
*/
@SuppressWarnings("serial")
public class LoginWindow extends LoginWindowLayout {
private final static Logger LOGGER = Logger.getLogger(LoginWindow.class);
public static enum LoginType {
ECP(0),
TEST_ACCOUNT(1),
DIRECT_CONNECT(2);
public final int id;
private LoginType(final int id) {
this.id = id;
}
}
// authentication method to use for login attempts
protected LoginType loginType = null;
// text constants
private final String NO_USERNAME = "Kein Benutzername angegeben!";
private final String NO_PASSWORD = "Kein Passwort angegeben!";
private boolean forceCustomSatellite = false;
public LoginWindow(Frame modalParent) {
// call the constructor of the superclass
super(modalParent);
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
Gui.exit(0);
}
});
// first do all listeners stuff
for (final LoginType type : LoginType.values()) {
rdoLoginType[type.id].setActionCommand(type.toString());
rdoLoginType[type.id].addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
cboOrganization.setEnabled(cboOrganization.getModel().getSize() != 0 && type == LoginType.ECP);
loginType = type;
btnOpenRegistration.setEnabled(type == LoginType.ECP);
}
}
});
}
// check if we had saved an authentication method
String savedAuthMethod = Config.getAuthenticationMethod();
LoginType savedLoginType;
try {
savedLoginType = LoginType.valueOf(savedAuthMethod);
} catch (Exception e) {
// if no valid LOGIN_TYPE was saved, just enable the BWIDM button
savedLoginType = LoginType.ECP;
}
if (savedLoginType == LoginType.ECP) {
// disable login button til the idp list is here
enableLogin(false);
}
// While filling, disable
cboOrganization.setEnabled(false);
// Not yet implemented, disable
rdoLoginType[LoginType.DIRECT_CONNECT.id].setEnabled(false);
// enable the corresponding button
rdoLoginType[savedLoginType.id].setSelected(true);
loginType = savedLoginType;
QuickTimer.scheduleOnce(new Task() {
List<Organization> orgs = null;
@Override
public void fire() {
try {
// Wait for proxy server init
App.waitForInit();
orgs = OrganizationCache.getAll();
} catch (Exception e) {
LOGGER.error("Error during execution: ", e);
}
// filter out every organisation without ecp
Iterator<Organization> iterator = orgs.iterator();
while (iterator.hasNext()) {
Organization current = iterator.next();
if (current == null || !current.isSetEcpUrl() || current.getEcpUrl().isEmpty())
iterator.remove();
}
// now send the organisations back to the LoginWindow
// through populateIdpCombo()
Gui.asyncExec(new Runnable() {
@Override
public void run() {
populateIdpCombo(orgs);
enableLogin(true);
}
});
}
});
btnLogin.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int mods = e.getModifiers();
// evaluate if SHIFT was hold during the click
forceCustomSatellite = ((mods & ActionEvent.SHIFT_MASK) == ActionEvent.SHIFT_MASK);
doLogin();
}
});
btnOpenRegistration.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
DesktopEnvironment.openWebpage(Link.REGISTER_BWIDM);
}
});
// make enter key activate login
pnlLoginForm.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "login");
pnlLoginForm.getActionMap().put("login", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent ae) {
btnLogin.doClick();
}
});
btnSettings.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ConfigWindow.open(LoginWindow.this);
}
});
btnLogDir.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
DesktopEnvironment.openLocal(new File(Config.getPath()));
}
});
txtUsername.getDocument().addDocumentListener(new TextChangeListener() {
@Override
public void changed() {
if (cboOrganization.getSelectedIndex() != -1)
return;
String name = txtUsername.getText();
int at = name.indexOf('@');
if (at == -1)
return;
final int oldCursorPos = txtUsername.getCaretPosition();
String suffix = name.substring(at + 1);
Organization organization = OrganizationCache.find(suffix);
if (organization == null)
return;
final String nameOnly = name.substring(0, at);
cboOrganization.setSelectedItem(organization);
Gui.asyncExec(new Runnable() {
@Override
public void run() {
txtUsername.setText(nameOnly);
txtUsername.setCaretPosition(Math.min(oldCursorPos, nameOnly.length()));
}
});
}
});
}
/**
* Called by the thread fetching the organization list from the cache
*
* @param orgs list of organization to show in the combo box
*/
public void populateIdpCombo(List<Organization> orgs) {
// sanity checks on orgs
if (orgs == null) {
LOGGER.error("No organizations received from the cache.");
return;
}
cboOrganization.setModel(new DefaultComboBoxModel<Organization>(orgs.toArray(new Organization[orgs.size()])));
// now check if we had a saved identity provider
String savedOrganizationId = Config.getIdentityProvider();
cboOrganization.setSelectedItem(OrganizationCache.find(savedOrganizationId));
cboOrganization.setEnabled(cboOrganization.getModel().getSize() != 0 && loginType == LoginType.ECP);
}
/**
* Saves various user choices to the config file.
* This gets triggered when the login button is activated.
*/
private void doSaveConfig() {
// first we need to check if the "Remember me" button is active
if (chkSaveUsername.isSelected()) {
// save username
String username = txtUsername.getText();
if (!username.isEmpty()) {
Config.setUsername(username);
}
} else {
Config.setUsername("");
}
// always save the authentication method and potentially the identity provider
Config.setAuthenticationMethod(loginType.toString());
// save the selected identity provider
Organization selectedOrg = cboOrganization.getItemAt(cboOrganization.getSelectedIndex());
if (selectedOrg != null) {
Config.setIdentityProvider(selectedOrg.organizationId);
}
}
/**
* Actually do the login using the username/password in the field using the
* authentication mechanism corresponding to the selected authButton
*
* @throws ECPAuthenticationException
*/
private void doLogin() {
// sanity check on loginType.
if (loginType == null) {
Gui.showMessageBox(this, "Bitte wählen Sie eine Authentifizierungsart.", MessageType.ERROR,
LOGGER, null);
return;
}
if (loginType == LoginType.ECP && cboOrganization.getSelectedIndex() == -1) {
Gui.showMessageBox(this, "Bitte wählen Sie ihre Organisation als 'Identity Provider'.",
MessageType.ERROR, LOGGER, null);
cboOrganization.requestFocusInWindow();
return;
}
// here we only check for the fields
String username = txtUsername.getText();
final String password = String.copyValueOf(txtPassword.getPassword());
// login clicked, lets first read the fields
if (username.isEmpty()) {
Gui.showMessageBox(this, NO_USERNAME, MessageType.ERROR, LOGGER, null);
return;
}
if (password.isEmpty()) {
Gui.showMessageBox(this, NO_PASSWORD, MessageType.ERROR, LOGGER, null);
return;
}
// determine which organization was selected by the user.
Organization selectedOrg = cboOrganization.getItemAt(cboOrganization.getSelectedIndex());
// now lets check if the username contains an organization
// for this we just check if the given username contains a '@'
// if it does, we just strip everything after and including '@'
if (loginType == LoginType.ECP && username.contains("@")) {
// split only on first occurence of '@'
String[] usernameSplit = username.split("@", 2);
if (!usernameSplit[1].isEmpty()) {
Organization orgInUsername = OrganizationCache.find(usernameSplit[1]);
if (orgInUsername != null) {
// username contains a known organization
if (!selectedOrg.equals(orgInUsername)) {
// but it does not match the one selected in the combobox
boolean ret = Gui.showMessageBox(
this,
"Der angegebene Benutzername enthält eine Organisation, die nicht mit Ihrer IDP-Auswahl übereinstimmt."
+ "\nWollen Sie die in Ihrem Benutzername gefundene Organisation verwenden?",
MessageType.QUESTION_YESNO, null, null);
if (ret) {
cboOrganization.setSelectedItem(orgInUsername);
selectedOrg = orgInUsername;
}
}
// always set the username to everything before '@'
txtUsername.setText(usernameSplit[0]);
username = usernameSplit[0];
}
}
}
// we are doing the login soon, first save the config
doSaveConfig();
// Setup login callback
final LoginWindow me = this;
final AuthenticatorCallback authenticatorCallback = new AuthenticatorCallback() {
@Override
public void postLogin(ReturnCode returnCode, final AuthenticationData data, Throwable t) {
switch (returnCode) {
case NO_ERROR:
Gui.asyncExec(new Runnable() {
@Override
public void run() {
postSuccessfulLogin(data);
}
});
return;
case IDENTITY_PROVIDER_ERROR:
Gui.showMessageBox(me, "IdP Error", MessageType.ERROR, LOGGER, null);
break;
case SERVICE_PROVIDER_ERROR:
// here if we have t != null then we have not received a token
// if we have t, then the token is invalid.
Gui.showMessageBox(me, "Invalid token from the service provider!", MessageType.ERROR,
LOGGER, t);
break;
case UNREGISTERED_ERROR:
LOGGER.error("User not registered!");
BwIdmLinkWindow.open((JFrame) SwingUtilities.getWindowAncestor(me));
break;
case INVALID_URL_ERROR:
Gui.showMessageBox(me, "ECP Authenticator says: Invalid URL.", MessageType.ERROR, LOGGER,
t);
break;
case GENERIC_ERROR:
default:
if (t == null || !t.getClass().equals(RuntimeException.class)) {
Gui.showMessageBox(me, "Internal error!", MessageType.ERROR, null, t);
} else {
Gui.showMessageBox(me, "Der Masterserver hat den Loginversuch mit der"
+ " folgenden Nachricht abgewiesen:\n\n"
+ t.getMessage(), MessageType.ERROR, null, null);
}
break;
}
enableLogin(true);
}
};
// now switch over the login types.
final Authenticator authenticator;
switch (loginType) {
case ECP:
authenticator = new EcpAuthenticator(selectedOrg.getEcpUrl());
break;
case TEST_ACCOUNT:
authenticator = new TestAccountAuthenticator();
break;
case DIRECT_CONNECT:
Gui.showMessageBox(this, "Not yet implemented", MessageType.ERROR, LOGGER, null);
return;
default:
Gui.showMessageBox(this, "No login type selected!", MessageType.ERROR, LOGGER, null);
return;
}
enableLogin(false);
final String finalUsername = username;
QuickTimer.scheduleOnce(new Task() {
@Override
public void fire() {
// Execute login
App.waitForInit();
try {
authenticator.login(finalUsername, password, authenticatorCallback);
return;
} catch (TException e) {
ThriftError.showMessage(LoginWindow.this, LOGGER, e, "Anmeldung fehlgeschlagen");
} catch (Exception e) {
Gui.showMessageBox(LoginWindow.this, "Anmeldung fehlgeschlagen", MessageType.ERROR,
LOGGER, e);
}
enableLogin(true);
}
});
}
/**
* Functions called by doLogin is the login process succeeded.
*
* @param data
*/
private void postSuccessfulLogin(AuthenticationData data) {
LOGGER.info(loginType.toString() + " succeeded, token " + data.satelliteToken);
// Update known suggested fingerprints
importFingerprints(data.satellites);
// now try to init the session with the data received
if (ThriftActions.initSession(data, forceCustomSatellite, SwingUtilities.getWindowAncestor(this))) {
if (chkSaveUsername.isSelected()) {
Config.saveCurrentSession(Session.getSatelliteAddress(), Session.getSatelliteToken(),
Session.getMasterToken());
}
dispose();
return;
}
enableLogin(true);
}
private void enableLogin(boolean enable) {
btnLogin.setEnabled(enable);
txtUsername.setEnabled(enable);
txtPassword.setEnabled(enable);
pnlLoginType.setEnabled(enable);
pnlLoginForm.setEnabled(enable);
chkSaveUsername.setEnabled(enable);
if (enable) {
// Do this here, otherwise the focus might not be set
String savedUsername = Config.getUsername();
if (savedUsername != null && !savedUsername.isEmpty()) {
txtUsername.setText(savedUsername);
chkSaveUsername.setSelected(true);
txtPassword.requestFocusInWindow();
} else {
txtUsername.requestFocusInWindow();
}
}
}
/**
* If master server supplies certificate finger prints for a satellite,
* store them so we can verify later.
*
* @param satellites
*/
private void importFingerprints(List<Satellite> satellites) {
if (satellites == null || satellites.isEmpty())
return;
for (Satellite sat : satellites) {
if (sat.addressList == null || sat.certSha256 == null || sat.addressList.isEmpty())
continue;
byte[] fingerprint = TBaseHelper.byteBufferToByteArray(sat.certSha256);
for (String address : sat.addressList) {
FingerprintManager.saveSuggestedFingerprint(address, fingerprint);
}
}
}
@SuppressWarnings("deprecation")
@Override
public void show() {
if (!isVisible()) {
pack();
MainWindow.centerShell(this);
}
super.show();
}
/**
* Opens the login window
*/
public static void open(Frame modalParent) {
LoginWindow win = new LoginWindow(modalParent);
win.setVisible(true);
}
}