/*******************************************************************************
* Copyright (c) 2015 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is 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:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.foundation.core.credentials.internal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.eclipse.equinox.security.storage.ISecurePreferences;
import org.eclipse.equinox.security.storage.SecurePreferencesFactory;
import org.eclipse.equinox.security.storage.StorageException;
import org.jboss.tools.foundation.core.credentials.UsernameChangedException;
import org.jboss.tools.foundation.core.internal.FoundationCorePlugin;
import org.jboss.tools.foundation.core.credentials.CredentialService;
import org.jboss.tools.foundation.core.credentials.ICredentialDomain;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
public class CredentialDomain implements ICredentialDomain {
// Properties used in the secure storage model
static final String PROPERTY_ID = "id";
static final String PROPERTY_NAME = "name";
static final String PROPERTY_REMOVABLE = "removable";
static final String PROPERTY_PASS = "pass";
static final String PROPERTY_DEFAULT_USER = "default.user";
static final String PROPERTY_USER_LIST = "user.list";
static final String PROPERTY_PROMPTED_USER_LIST = "user.list.prompted";
static final String NOT_LOADED_PASSWORD = "********";
private String userVisibleName;
private String id, defaultUsername;
private boolean removable;
private HashMap<String, String> credentials;
private ArrayList<String> promptedCredentials;
public CredentialDomain(String id, String name, boolean removable) {
if(id == null) {
throw new IllegalArgumentException("Id cannot be null.");
}
this.id = id;
this.userVisibleName = name;
this.removable = removable;
this.defaultUsername = null;
this.credentials = new HashMap<String, String>();
this.promptedCredentials = new ArrayList<String>();
}
public CredentialDomain(Preferences pref) throws BackingStoreException {
this.id = pref.get(PROPERTY_ID, ""); //Id cannot be null.
this.userVisibleName = pref.get(PROPERTY_NAME, (String)null);;
this.removable = pref.getBoolean(PROPERTY_REMOVABLE, true);
this.defaultUsername = pref.get(PROPERTY_DEFAULT_USER, (String)null);
credentials = new HashMap<String, String>();
String usersList = pref.get(PROPERTY_USER_LIST, (String)null);
if( usersList != null && !usersList.isEmpty()) {
String[] users = (usersList == null ? new String[0] : usersList.split("\n"));
for( int i = 0; i < users.length; i++ ) {
credentials.put(users[i], NOT_LOADED_PASSWORD);
}
}
String promptedUserList = pref.get(PROPERTY_PROMPTED_USER_LIST, (String)null);
promptedCredentials = new ArrayList<String>();
if(promptedUserList != null && !promptedUserList.isEmpty()) {
String[] users = (usersList == null ? new String[0] : promptedUserList.split("\n"));
promptedCredentials.addAll(Arrays.asList(users));
}
if( defaultUsername == null || !userExists(defaultUsername)) {
// The default name doesn't exist, so we need another.
String[] users = getUsernames();
if( users.length > 0) {
defaultUsername = users[0];
}
}
}
public String getId() {
return id;
}
public boolean getRemovable() {
return removable;
}
/**
* Since returned value is used by UI, returning null may cause NPE,
* and therefore should be checked for null at each call.
* It is better not to return null.
*/
@Override
public String getName() {
return emptyOrNull(userVisibleName) ? (emptyOrNull(id) ? "" : id) : userVisibleName;
}
private boolean emptyOrNull(String s) {
return s == null ? true : s.isEmpty();
}
public boolean userExists(String user) {
return credentials.containsKey(user) || promptedCredentials.contains(user);
}
public boolean userRequiresPrompt(String user) {
return promptedCredentials.contains(user);
}
public String[] getUsernames() {
SortedSet<String> ret = new TreeSet<String>();
ret.addAll(credentials.keySet());
ret.addAll(promptedCredentials);
return (String[]) ret.toArray(new String[ret.size()]);
}
protected void addCredentials(String user, String pass) {
if( defaultUsername == null )
defaultUsername = user;
promptedCredentials.remove(user);
credentials.put(user, pass);
}
protected void addPromptedCredentials(String user) {
if( defaultUsername == null )
defaultUsername = user;
credentials.remove(user);
promptedCredentials.add(user);
}
protected void removeCredential(String user) {
credentials.remove(user);
promptedCredentials.remove(user);
if( user.equals(defaultUsername)) {
String[] usernames = getUsernames();
if( usernames.length == 0 ) {
defaultUsername = null;
} else {
defaultUsername = usernames[0];
}
}
}
public String getCredentials(String user) throws StorageException, UsernameChangedException {
return getCredentials(user, true);
}
public String getPassword(String user) throws StorageException {
try {
return getCredentials(user, false);
} catch(UsernameChangedException uce) {
// Should never happen
FoundationCorePlugin.pluginLog().logError("User attempted to change username when not allowed", uce);
}
return null;
}
public String getCredentials(String user, boolean canChangeUser) throws StorageException, UsernameChangedException {
if( userExists(user)) {
if( !userRequiresPrompt(user)) {
String ret = credentials.get(user);
if( NOT_LOADED_PASSWORD.equals(ret)) {
ISecurePreferences secureRoot = SecurePreferencesFactory.getDefault();
ISecurePreferences secureCredentialRoot = secureRoot.node(CredentialsModel.CREDENTIAL_BASE_KEY);
ISecurePreferences secureDomain = secureCredentialRoot.node(getId());
ISecurePreferences secureUser = secureDomain.node(user);
ret = secureUser.get(PROPERTY_PASS, (String)null);
credentials.put(user, ret);
}
return ret;
}
}
if( canChangeUser ) {
return CredentialsModel.getDefault().promptForCredentials(this, user);
} else if( user != null){
return CredentialsModel.getDefault().promptForPassword(this, user);
} else {
return null;
}
}
private String getCredentialsForSave(String user) {
if( !userRequiresPrompt(user)) {
String ret = credentials.get(user);
if( NOT_LOADED_PASSWORD.equals(ret)) {
return null;
}
return ret;
} else {
return null;
}
}
void saveToPreferences(Preferences prefs, ISecurePreferences securePrefs) throws StorageException {
prefs.put(PROPERTY_ID, id);
prefs.put(PROPERTY_NAME, getName());
prefs.putBoolean(PROPERTY_REMOVABLE, removable);
if( defaultUsername != null )
prefs.put(PROPERTY_DEFAULT_USER, defaultUsername);
Set<String> users = credentials.keySet();
String[] userList = (String[]) users.toArray(new String[users.size()]);
prefs.put(PROPERTY_USER_LIST, String.join("\n", userList));
String[] childNodes = securePrefs.childrenNames();
for( int i = 0; i < childNodes.length; i++ ) {
// Delete old nodes that are no longer in the model
ISecurePreferences userNode = securePrefs.node(childNodes[i]);
if( !users.contains(childNodes[i])) {
userNode.removeNode();
} else {
// Goal here is to force a secure-storage event
userNode.get(PROPERTY_PASS, (String)null);
}
}
// Save the password securely
for( int i = 0; i < userList.length; i++ ) {
String user = userList[i];
ISecurePreferences userNode = securePrefs.node(user);
String forSave = getCredentialsForSave(user);
if( forSave != null ) {
userNode.put(PROPERTY_PASS, forSave, true);
}
}
String[] promptedUsers = (String[]) promptedCredentials.toArray(new String[promptedCredentials.size()]);
prefs.put(PROPERTY_PROMPTED_USER_LIST, String.join("\n", promptedUsers));
}
public String getDefaultUsername() {
return defaultUsername;
}
public void setDefaultUsername(String user) throws IllegalArgumentException {
if( !userExists(user)) {
throw new IllegalArgumentException("User " + user + " does not exist for this domain.");
}
defaultUsername = user;
}
}