package org.openslx.dozmod.gui.window; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; 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 java.util.UUID; 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.logging.log4j.LogManager; import org.apache.logging.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.Branding; import org.openslx.dozmod.Config; import org.openslx.dozmod.authentication.Authenticator; import org.openslx.dozmod.authentication.BrowserAuthenticator; 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.activity.UpdatePanel; import org.openslx.dozmod.gui.helper.I18n; 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.ClientVersion; import org.openslx.dozmod.util.DesktopEnvironment; import org.openslx.dozmod.util.DesktopEnvironment.Link; import org.openslx.util.QuickTimer; import org.openslx.util.Util; import org.openslx.util.QuickTimer.Task; import edu.kit.scc.dei.ecplean.ECPAuthenticationException; /** * @author Jonathan Bauer * */ public class LoginWindow extends LoginWindowLayout { /** * Version for serialization. */ private static final long serialVersionUID = 3826201134186162076L; private final static Logger LOGGER = LogManager.getLogger(LoginWindow.class); public static enum LoginType { ECP(0), TEST_ACCOUNT(1), DIRECT_CONNECT(2), EXTERNAL_BROWSER(3); public final int id; private LoginType(final int id) { this.id = id; } } // authentication method to use for login attempts protected LoginType loginType = null; // Currently running authentication protected Authenticator currentAuthenticator; 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) return; cboOrganization.setEnabled(cboOrganization.getModel().getSize() != 0 && type == LoginType.ECP); loginType = type; btnOpenRegistration.setEnabled(type == LoginType.ECP || type == LoginType.EXTERNAL_BROWSER); boolean browser = (type == LoginType.EXTERNAL_BROWSER); cboOrganization.setVisible(!browser); txtUsername.setVisible(!browser); txtPassword.setVisible(!browser); lblOrganization.setVisible(!browser); lblUsername.setVisible(!browser); lblPassword.setVisible(!browser); txtUrl.setVisible(browser); lblError.setText("-"); lblError.setVisible(browser); pnlLoginForm.doLayout(); if (currentAuthenticator != null) { currentAuthenticator.cancel(); currentAuthenticator = null; } } }); } // 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 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 iterator = orgs.iterator(); while (iterator.hasNext()) { Organization current = iterator.next(); if (current == null || !current.isSetEcpUrl() || Util.isEmptyString(current.getEcpUrl())) { iterator.remove(); } } // now send the organisations back to the LoginWindow // through populateIdpCombo() Gui.asyncExec(new Runnable() { @Override public void run() { populateIdpCombo(orgs); LoginWindow.this.validate(); LoginWindow.this.pack(); 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.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK), "login"); pnlLoginForm.getActionMap().put("login", new AbstractAction() { /** * */ private static final long serialVersionUID = 5794599288256601257L; @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 || loginType != LoginType.ECP) 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())); } }); } }); QuickTimer.scheduleOnce(new Task() { @Override public void fire() { if (!ClientVersion.isNewest()) { Gui.asyncExec(new Runnable() { @Override public void run() { pnlActivity.add(new UpdatePanel(ClientVersion.getRemoteRevision(), false)); pnlActivity.setVisible(true); LoginWindow.this.validate(); LoginWindow.this.pack(); } }); } } }); } /** * 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 orgs) { // sanity checks on orgs if (orgs == null) { LOGGER.error("No organizations received from the cache."); return; } cboOrganization.setModel(new DefaultComboBoxModel(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, I18n.WINDOW.getString("Login.Message.error.noLoginType"), MessageType.ERROR, LOGGER, null); return; } if (loginType == LoginType.ECP && cboOrganization.getSelectedIndex() == -1) { Gui.showMessageBox(this, I18n.WINDOW.getString("Login.Message.error.noOrganization"), MessageType.ERROR, LOGGER, null); cboOrganization.requestFocusInWindow(); return; } // here we only check for the fields String username; final String password; // SPECIAL CASE - browser based // username is the full URL including token, password is just the token if (loginType == LoginType.EXTERNAL_BROWSER) { password = UUID.randomUUID().toString(); username = "https://" + Branding.getMasterServerAddress() + "/webif/shib/?do=SuiteLogin&accessToken=" + password; lblError.setText(I18n.WINDOW.getString("Login.Label.error.continueBrowser")); txtUrl.setText(username); } else { username = txtUsername.getText(); password = String.copyValueOf(txtPassword.getPassword()); } // login clicked, lets first read the fields if (username.isEmpty()) { Gui.showMessageBox(this, I18n.WINDOW.getString("Login.Message.error.noUsername"), MessageType.ERROR, LOGGER, null); return; } if (password.isEmpty()) { Gui.showMessageBox(this, I18n.WINDOW.getString("Login.Message.error.noPassword"), MessageType.ERROR, LOGGER, null); return; } // determine which organization was selected by the user. Organization selectedOrg = cboOrganization.getItemAt(cboOrganization.getSelectedIndex()); // 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) { if (t != null) { lblError.setText(t.getMessage()); } switch (returnCode) { case NO_ERROR: Gui.asyncExec(new Runnable() { @Override public void run() { postSuccessfulLogin(data); } }); return; case IDENTITY_PROVIDER_ERROR: Gui.showMessageBox(me, I18n.WINDOW.getString("Login.Message.error.authIdentityProvider"), 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, I18n.WINDOW.getString("Login.Message.error.authServiceProvider"), 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, I18n.WINDOW.getString("Login.Message.error.authInvalidURL"), MessageType.ERROR, LOGGER, t); break; case GENERIC_ERROR: default: if (t == null || !t.getClass().equals(RuntimeException.class)) { Gui.showMessageBox(me, I18n.WINDOW.getString("Login.Message.error.authInternal"), MessageType.ERROR, null, t); } else { Gui.showMessageBox(me, I18n.WINDOW.getString("Login.Message.error.authMasterServer", t.getMessage()), MessageType.ERROR, null, null); } break; } enableLogin(true); } }; // now switch over the login types. switch (loginType) { case ECP: currentAuthenticator = new EcpAuthenticator(selectedOrg.getEcpUrl()); break; case TEST_ACCOUNT: currentAuthenticator = new TestAccountAuthenticator(); break; case EXTERNAL_BROWSER: currentAuthenticator = new BrowserAuthenticator(lblError); break; case DIRECT_CONNECT: Gui.showMessageBox(this, I18n.WINDOW.getString("Login.Message.error.loginTypeDirectConnect"), MessageType.ERROR, LOGGER, null); return; default: Gui.showMessageBox(this, I18n.WINDOW.getString("Login.Message.error.loginTypeDefault"), MessageType.ERROR, LOGGER, null); return; } enableLogin(false); final String finalUsername = username; QuickTimer.scheduleOnce(new Task() { @Override public void fire() { // Execute login App.waitForInit(); try { currentAuthenticator.login(finalUsername, password, authenticatorCallback); return; } catch (TException e) { ThriftError.showMessage(LoginWindow.this, LOGGER, e, I18n.WINDOW.getString("Login.Message.error.loginFailed")); } catch (Exception e) { Gui.showMessageBox(LoginWindow.this, I18n.WINDOW.getString("Login.Message.error.loginFailed"), MessageType.ERROR, LOGGER, e); } finally { currentAuthenticator = null; } 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 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); } } } @Override public void setVisible(boolean visible) { if (!isVisible()) { pack(); MainWindow.centerShell(this); } super.setVisible(visible); } /** * Opens the login window */ public static void open(Frame modalParent) { LoginWindow win = new LoginWindow(modalParent); win.setVisible(true); } }