/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.security; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.rapidminer.io.Base64; import com.rapidminer.io.process.XMLTools; import com.rapidminer.tools.FileSystemService; import com.rapidminer.tools.GlobalAuthenticator; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.XMLException; /** * The Wallet stores user credentials (username and passwords). It is used by the * {@link GlobalAuthenticator} and can be edited with the {@link PasswordManager}. * {@link UserCredential}s are stored per {@link URL}. * * Note: Though this class has a {@link #getInstance()} method, it is not a pure singleton. In fact, * the {@link PasswordManager} sets a new instance via {@link #setInstance(Wallet)} after editing. * * @author Miguel Buescher, Marco Boeck * */ @SuppressWarnings("deprecation") public class Wallet { private static final String CACHE_FILE_NAME = "secrets.xml"; private HashMap<String, UserCredential> wallet = new HashMap<String, UserCredential>(); private static final String ID_PREFIX_URL_SEPERATOR = "___"; public static final String ID_MARKETPLACE = "Marketplace"; private static Wallet instance = new Wallet(); static { instance.readCache(); } /** * Singleton access. * * @return */ public static Wallet getInstance() { return instance; } public static void setInstance(Wallet wallet) { instance = wallet; } /** * Reads the wallet from the secrets.xml file in the user's home directory. */ public void readCache() { final File userConfigFile = FileSystemService.getUserConfigFile(CACHE_FILE_NAME); if (!userConfigFile.exists()) { return; } LogService.getRoot().log(Level.CONFIG, "com.rapidminer.gui.security.Wallet.reading_secrets_file"); Document doc; try { doc = XMLTools.parse(userConfigFile); } catch (Exception e) { LogService.getRoot().log( Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.gui.security.Wallet.reading_secrets_file_error", e), e); return; } readCache(doc); } /** * Reads the wallet from a {@link Document} object. * * @param doc * the document which contains the secrets */ public void readCache(Document doc) { NodeList secretElems = doc.getDocumentElement().getElementsByTagName("secret"); UserCredential usercredential; for (int i = 0; i < secretElems.getLength(); i++) { Element secretElem = (Element) secretElems.item(i); String id = XMLTools.getTagContents(secretElem, "id"); String url = XMLTools.getTagContents(secretElem, "url"); String user = XMLTools.getTagContents(secretElem, "user"); char[] password; try { password = new String(Base64.decode(XMLTools.getTagContents(secretElem, "password"))).toCharArray(); } catch (IOException e) { LogService.getRoot().log( Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.gui.security.Wallet.reading_entry_in_secrets_file_error", e), e); continue; } usercredential = new UserCredential(url, user, password); if (id != null) { // new entries with an id wallet.put(buildKey(id, usercredential.getURL()), usercredential); } else { // for old entries which do not have an id wallet.put(usercredential.getURL(), usercredential); } } } /** * Adds the given credentials. * * @deprecated use {@link #registerCredentials(String, UserCredential)} instead. * @param authentication */ @Deprecated public void registerCredentials(UserCredential authentication) { wallet.put(authentication.getURL(), authentication); } /** * Adds the given credentials with the given ID. ID is necessary because there may be more * credentials than one for the same URL. * * @param id * @param authentication * @throws IllegalArgumentException * if the key is <code>null</code> */ public void registerCredentials(String id, UserCredential authentication) throws IllegalArgumentException { if (id == null) { throw new IllegalArgumentException("id must not be null!"); } wallet.put(buildKey(id, authentication.getURL()), authentication); // we need to consider possibility of an old entry for the same URL, remove it here because // it got replaced if (getEntry(authentication.getURL()) != null) { removeEntry(authentication.getURL()); } } /** * Returns the number of entries in the {@link Wallet}. * * @return */ public int size() { return wallet.size(); } /** * Deep clone. */ @Override public Wallet clone() { Wallet clone = new Wallet(); for (String key : this.getKeys()) { String[] urlAndMaybeID = key.split(ID_PREFIX_URL_SEPERATOR); if (urlAndMaybeID.length == 2) { // this is for new keys which have an ID prefix UserCredential entry = wallet.get(buildKey(urlAndMaybeID[0], urlAndMaybeID[1])); clone.registerCredentials(urlAndMaybeID[0], entry.clone()); } else { // old keys which do not yet have the ID prefix UserCredential entry = wallet.get(key); clone.registerCredentials(entry.clone()); } } return clone; } /** * Returns a {@link List} of {@link String} keys in this {@link Wallet}. * * @return */ public LinkedList<String> getKeys() { Iterator<String> it = wallet.keySet().iterator(); LinkedList<String> keyset = new LinkedList<String>(); while (it.hasNext()) { keyset.add(it.next()); } return keyset; } /** * Returns the {@link UserCredential} for the given url {@link String} or <code>null</code> if * there is no key matching this url. * * @deprecated use {@link #getEntry(String, String)} instead. * @param url * @return */ @Deprecated public UserCredential getEntry(String url) { return wallet.get(url); } /** * Returns the {@link UserCredential} for the given id and url {@link String}s. If there is no * key matching the given id and url tries to return the {@link UserCredential} for the given * url (fallback for old entries). If both fail, returns <code>null</code>. * * @param id * @param url * @return */ public UserCredential getEntry(String id, String url) { UserCredential credentials = wallet.get(buildKey(id, url)); if (credentials == null) { // fallback for old entries which can supply a null key credentials = wallet.get(url); } return credentials; } /** * Removes the {@link UserCredential} for the given url {@link String}. * * @deprecated use {@link #removeEntry(String, String)} instead. * @param url */ @Deprecated public void removeEntry(String url) { wallet.remove(url); } /** * Removes the {@link UserCredential} for the given id and url {@link String}s. * * @param id * @param url */ public void removeEntry(String id, String url) { wallet.remove(buildKey(id, url)); } /** * Creates a XML representation of the wallet. * * @return The XML document. */ public Document getWalletAsXML() { Document doc = XMLTools.createDocument(); Element root = doc.createElement(CACHE_FILE_NAME); doc.appendChild(root); for (String key : getKeys()) { Element entryElem = doc.createElement("secret"); root.appendChild(entryElem); String[] urlAndMaybeID = key.split(ID_PREFIX_URL_SEPERATOR); if (urlAndMaybeID.length == 2) { // this is for new keys which have an ID prefix XMLTools.setTagContents(entryElem, "id", urlAndMaybeID[0]); XMLTools.setTagContents(entryElem, "url", urlAndMaybeID[1]); } else { // old keys which do not yet have the ID prefix XMLTools.setTagContents(entryElem, "url", key); } XMLTools.setTagContents(entryElem, "user", wallet.get(key).getUsername()); XMLTools.setTagContents(entryElem, "password", Base64.encodeBytes(new String(wallet.get(key).getPassword()).getBytes())); } return doc; } /** * Saves the wallet to the secrets.xml file in the users home directory. */ public void saveCache() { LogService.getRoot().log(Level.CONFIG, "com.rapidminer.gui.security.Wallet.saving_secrets_file"); Document doc = getWalletAsXML(); File file = FileSystemService.getUserConfigFile(CACHE_FILE_NAME); try { XMLTools.stream(doc, file, null); } catch (XMLException e) { LogService.getRoot().log( Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.gui.security.Wallet.saving_secrets_file_error", e), e); } } /** * Builds the {@link String} from an ID and an URL {@link String} which is used to * store/retrieve entries in the {@link Wallet}. * * @param id * @param url * @return */ private String buildKey(String id, String url) { return id + ID_PREFIX_URL_SEPERATOR + url; } /** * Returns the id {@link String} contained in the key or <code>null</code> if there is no id. * * @param key * @return */ public String extractIdFromKey(String key) { String[] urlAndMaybeID = key.split(ID_PREFIX_URL_SEPERATOR); if (urlAndMaybeID.length == 2) { return urlAndMaybeID[0]; } else { return null; } } }