changeset 1776:36c9ffed683c

Make setup work if webapp is not installed. Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-September/015812.html PR2581
author Severin Gehwolf <sgehwolf@redhat.com>
date Mon, 07 Sep 2015 16:44:52 +0200
parents 040e20c32a64
children 19b2b582ff98
files setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/MongoUserSetupView.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/SetupWindow.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/StartView.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/AuthFileWriter.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/CredentialsWriter.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/KeyringWriter.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/MongodbUserSetup.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/ThermostatSetup.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/ThermostatUserSetup.java setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/AuthFileWriterTest.java setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/KeyringWriterTest.java setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/MongodbUserSetupTest.java setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/ThermostatSetupTest.java
diffstat 13 files changed, 653 insertions(+), 159 deletions(-) [+]
line wrap: on
line diff
--- a/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/MongoUserSetupView.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/MongoUserSetupView.java	Mon Sep 07 16:44:52 2015 +0200
@@ -36,7 +36,9 @@
 
 package com.redhat.thermostat.setup.command.internal;
 
+import com.redhat.thermostat.setup.command.internal.model.ThermostatSetup;
 import com.redhat.thermostat.setup.command.locale.LocaleResources;
+import com.redhat.thermostat.shared.locale.LocalizedString;
 import com.redhat.thermostat.shared.locale.Translate;
 
 import javax.swing.BorderFactory;
@@ -48,6 +50,7 @@
 import javax.swing.JPanel;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
+
 import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Dimension;
@@ -56,6 +59,7 @@
 
 public class MongoUserSetupView extends JPanel implements SetupView {
 
+    private final ThermostatSetup thermostatSetup;
     private JButton backBtn;
     private JButton nextBtn;
     private JButton cancelBtn;
@@ -65,12 +69,13 @@
     private InputCredentialPanel credentialPanel;
 
     private static final String THERMOSTAT_LOGO = "thermostat.png";
-    private static final String PROGRESS = "Step 2 of 3";
+    private static final String PROGRESS_FORMAT = "Step 2 of %s";
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
-    public MongoUserSetupView(LayoutManager layout) {
+    public MongoUserSetupView(LayoutManager layout, ThermostatSetup setup) {
         super(layout);
 
+        this.thermostatSetup = setup;
         createMidPanel();
         createToolbarPanel();
 
@@ -85,7 +90,11 @@
 
     @Override
     public void setProgress(JLabel progress) {
-        progress.setText(PROGRESS);
+        int totalSteps = 2;
+        if (thermostatSetup.isWebAppInstalled()) {
+            totalSteps++;
+        }
+        progress.setText(String.format(PROGRESS_FORMAT, totalSteps));
     }
 
     private void createMidPanel() {
@@ -128,7 +137,11 @@
 
         backBtn = new JButton(translator.localize(LocaleResources.BACK).getContents());
         backBtn.setPreferredSize(new Dimension(70, 30));
-        nextBtn = new JButton(translator.localize(LocaleResources.NEXT).getContents());
+        LocalizedString nextButtonLabel = translator.localize(LocaleResources.NEXT);
+        if (!thermostatSetup.isWebAppInstalled()) {
+            nextButtonLabel = translator.localize(LocaleResources.FINISH);
+        }
+        nextBtn = new JButton(nextButtonLabel.getContents());
         nextBtn.setPreferredSize(new Dimension(70, 30));
         nextBtn.setEnabled(false);
         cancelBtn = new JButton(translator.localize(LocaleResources.CANCEL).getContents());
--- a/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/SetupWindow.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/SetupWindow.java	Mon Sep 07 16:44:52 2015 +0200
@@ -214,8 +214,8 @@
         mainView.add(topPanel, BorderLayout.NORTH);
         frame.add(mainView);
 
-        startView = new StartView(new BorderLayout());
-        mongoUserSetupView = new MongoUserSetupView(new BorderLayout());
+        startView = new StartView(new BorderLayout(), thermostatSetup);
+        mongoUserSetupView = new MongoUserSetupView(new BorderLayout(), thermostatSetup);
         userPropertiesView = new UserPropertiesView(new BorderLayout());
         setupCompleteView = new SetupCompleteView(new BorderLayout());
     }
@@ -353,8 +353,10 @@
                 userPropertiesView.disableButtons();
                 thermostatSetup.createMongodbUser(storageUsername, storagePassword);
                 try {
-                    thermostatSetup.createAgentUser(agentUsername, agentPassword);
-                    thermostatSetup.createClientAdminUser(clientUsername, clientPassword);
+                    if (thermostatSetup.isWebAppInstalled()) {
+                        thermostatSetup.createAgentUser(agentUsername, agentPassword);
+                        thermostatSetup.createClientAdminUser(clientUsername, clientPassword);
+                    }
                     thermostatSetup.commit();
                     return null;
                 } catch (IOException e) {
--- a/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/StartView.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/StartView.java	Mon Sep 07 16:44:52 2015 +0200
@@ -37,10 +37,11 @@
 package com.redhat.thermostat.setup.command.internal;
 
 import com.redhat.thermostat.client.swing.components.ThermostatScrollPane;
+import com.redhat.thermostat.setup.command.internal.model.ThermostatSetup;
 import com.redhat.thermostat.common.ApplicationInfo;
 import com.redhat.thermostat.setup.command.locale.LocaleResources;
 import com.redhat.thermostat.shared.locale.Translate;
-
+ 
 import javax.swing.BorderFactory;
 import javax.swing.Box;
 import javax.swing.BoxLayout;
@@ -67,6 +68,7 @@
 
 public class StartView extends JPanel implements SetupView {
 
+    private final ThermostatSetup thermostatSetup;
     private JButton nextBtn;
     private JButton cancelBtn;
     private JButton moreInfoBtn;
@@ -81,12 +83,12 @@
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
     private static final String SHOW_MORE = "Show More";
     private static final String SHOW_LESS = "Show Less";
-    private static final String PROGRESS = "Step 1 of 3";
-    private static final String FAST_TRACK_PROGRESS = "Step 1 of 2";
+    private static final String PROGRESS_FORMAT = "Step 1 of %s";
 
-    public StartView(LayoutManager layout) {
+    public StartView(LayoutManager layout, ThermostatSetup thermostatSetup) {
         super(layout);
 
+        this.thermostatSetup = thermostatSetup;
         createToolbarPanel();
         createMidPanel();
     }
@@ -98,10 +100,14 @@
 
     @Override
     public void setProgress(JLabel progress) {
+        int totalSteps = 2;
         if (quickSetupBtn.isSelected()) {
-            progress.setText(FAST_TRACK_PROGRESS);
+            progress.setText(String.format(PROGRESS_FORMAT, totalSteps));
         } else {
-            progress.setText(PROGRESS);
+            if (thermostatSetup.isWebAppInstalled()) {
+                totalSteps++;
+            }
+            progress.setText(String.format(PROGRESS_FORMAT, totalSteps));
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/AuthFileWriter.java	Mon Sep 07 16:44:52 2015 +0200
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.setup.command.internal.model;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Properties;
+
+import com.redhat.thermostat.shared.config.CommonPaths;
+
+class AuthFileWriter extends CredentialsWriter {
+    
+    private final CommonPaths paths;
+    private final CredentialsFileCreator creator;
+    
+    AuthFileWriter(CommonPaths paths, CredentialsFileCreator creator) {
+        this.paths = paths;
+        this.creator = creator;
+    }
+
+    public void write() throws IOException {
+        char[] password = getPassword();
+        String username = getUsername();
+        Objects.requireNonNull(password);
+        Objects.requireNonNull(username);
+        Properties credentialProps = new Properties();
+        credentialProps.setProperty("username", username);
+        credentialProps.setProperty("password", String.valueOf(password));
+        File credentialsFile = paths.getUserAgentAuthConfigFile();
+        creator.create(credentialsFile);
+        try (FileOutputStream fout = new FileOutputStream(credentialsFile)) {
+            credentialProps.store(fout, "Credentials used for 'thermostat agent' connections.");
+        } finally {
+            Arrays.fill(password, '\0');
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/CredentialsWriter.java	Mon Sep 07 16:44:52 2015 +0200
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.setup.command.internal.model;
+
+import java.io.IOException;
+
+abstract class CredentialsWriter {
+
+    private String username;
+    private char[] password;
+    
+    public void setCredentials(String username, char[] password) {
+        this.username = username;
+        this.password = password;
+    }
+    
+    protected String getUsername() {
+        return username;
+    }
+    
+    protected char[] getPassword() {
+        return password;
+    }
+    
+    /**
+     * persists storage credentials.
+     */
+    public abstract void write() throws IOException;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/KeyringWriter.java	Mon Sep 07 16:44:52 2015 +0200
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.setup.command.internal.model;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.redhat.thermostat.common.config.ClientPreferences;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.utils.keyring.Keyring;
+import com.redhat.thermostat.utils.keyring.KeyringException;
+
+class KeyringWriter extends CredentialsWriter {
+    
+    private static final Logger logger = LoggingUtils.getLogger(KeyringWriter.class);
+    private final ClientPreferences prefs;
+    private final Keyring keyring;
+    private String storageUrl;
+    
+    KeyringWriter(ClientPreferences prefs, Keyring keyring) {
+        this.prefs = prefs;
+        this.keyring = keyring;
+    }
+    
+    void setStorageUrl(String customStorageUrl) {
+        this.storageUrl = customStorageUrl;
+    }
+    
+    public void write() throws IOException {
+        String username = getUsername();
+        char[] password = getPassword();
+        Objects.requireNonNull(username);
+        Objects.requireNonNull(password);
+        Objects.requireNonNull(storageUrl);
+        try {
+            prefs.setSaveEntitlements(true); // force writing on flush()
+            prefs.setConnectionUrl(storageUrl);
+            prefs.setUserName(username);
+            // Unconditionally save credentials for the chosen username. If setup
+            // runs again it will overwrite existing credentials.
+            prefs.flush();
+            keyring.savePassword(storageUrl, username, password);
+        } catch (KeyringException e) {
+            // Don't fail setup if storing to keyring fails. After all this is
+            // for convenience only (so that thermostat gui works out of the box
+            // after setup). If we failed, one would have to have a working
+            // keyring setup which isn't always the case. Think thermostat agent.
+            logger.log(Level.INFO, "Failed to store client credentials to keyring. Usability of client commands might suffer.", e);
+        } finally {
+            Arrays.fill(password, '\0');
+        }
+    }
+}
--- a/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/MongodbUserSetup.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/MongodbUserSetup.java	Mon Sep 07 16:44:52 2015 +0200
@@ -71,11 +71,13 @@
     private final CommonPaths paths;
     private final StampFiles stampFiles;
     private final StructureInformation structureInfo;
+    private final AuthFileWriter authFileWriter;
+    private final KeyringWriter keyringWriter;
     private String username;
     private char[] password;
     private String userComment;
     
-    MongodbUserSetup(UserCredsValidator validator, Launcher launcher, CredentialFinder finder, CredentialsFileCreator fileCreator, CommonPaths paths, StampFiles stampFiles, StructureInformation structureInfo) {
+    MongodbUserSetup(UserCredsValidator validator, Launcher launcher, CredentialFinder finder, CredentialsFileCreator fileCreator, CommonPaths paths, StampFiles stampFiles, StructureInformation structureInfo, AuthFileWriter authWriter, KeyringWriter keyringWriter) {
         this.validator = validator;
         this.launcher = launcher;
         this.finder = finder;
@@ -83,6 +85,8 @@
         this.stampFiles = stampFiles;
         this.paths = paths;
         this.structureInfo = structureInfo;
+        this.authFileWriter = authWriter;
+        this.keyringWriter = keyringWriter;
     }
     
     @Override
@@ -132,6 +136,9 @@
             // started successfully.
             stopStorage();
             if (!isWebAppInstalled()) {
+                // Allow for gui/agent to work out of the box by using mongodb
+                // URLs.
+                setupForDirectMongodbUrls();
                 String completeDate = ThermostatSetup.DATE_FORMAT.format(new Date());
                 String regularContent = "Created by '" + ThermostatSetup.PROGRAM_NAME + "' on " + completeDate;
                 stampFiles.createSetupCompleteStamp(regularContent);
@@ -146,6 +153,18 @@
         }
     }
     
+    private void setupForDirectMongodbUrls() throws MongodbUserSetupException {
+        try {
+            authFileWriter.setCredentials(username, Arrays.copyOf(password, password.length));
+            authFileWriter.write();
+            keyringWriter.setCredentials(username, Arrays.copyOf(password, password.length));
+            keyringWriter.setStorageUrl(ThermostatSetup.MONGODB_STORAGE_URL);
+            keyringWriter.write();
+        } catch (IOException e) {
+            throw new MongodbUserSetupException("Error creating agent.auth file or persisting keyring prefs", e);
+        }
+    }
+
     private void cleanupAndReThrow(boolean storageStarted, MongodbUserSetupException e) throws MongodbUserSetupException {
         if (storageStarted) {
             stopStorage();
--- a/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/ThermostatSetup.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/ThermostatSetup.java	Mon Sep 07 16:44:52 2015 +0200
@@ -36,27 +36,19 @@
 
 package com.redhat.thermostat.setup.command.internal.model;
 
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 
 import com.redhat.thermostat.common.config.ClientPreferences;
-import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.launcher.Launcher;
 import com.redhat.thermostat.shared.config.CommonPaths;
 import com.redhat.thermostat.utils.keyring.Keyring;
-import com.redhat.thermostat.utils.keyring.KeyringException;
 
 public class ThermostatSetup implements PersistableSetup {
 
-    private static final Logger logger = LoggingUtils.getLogger(ThermostatSetup.class);
+    static final String WEB_STORAGE_URL = "http://127.0.0.1:8999/thermostat/storage";
+    static final String MONGODB_STORAGE_URL = "mongodb://127.0.0.1:27518";
     static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss zzz");
     static final String PROGRAM_NAME = "thermostat setup";
     private static final String THERMOSTAT_AGENT_REC_ROLE_NAME = "thermostat-agent";
@@ -66,23 +58,15 @@
     private final ThermostatUserSetup userSetup;
     private final MongodbUserSetup mongodbUserSetup;
     private final StructureInformation structureInfo;
-    private final CredentialsFileCreator creator;
-    private final CommonPaths paths;
-    private final Keyring keyring;
-    private final ClientPreferences prefs;
-    private String agentUserName;
-    private char[] agentPassword;
-    private String clientUsername;
-    private char[] clientPassword;
+    private final AuthFileWriter authWriter;
+    private final KeyringWriter keyringWriter;
     
-    ThermostatSetup(ThermostatUserSetup userSetup, MongodbUserSetup mongodbUserSetup, StructureInformation structureInfo, CommonPaths paths, CredentialsFileCreator creator, Keyring keyring, ClientPreferences prefs) {
+    ThermostatSetup(ThermostatUserSetup userSetup, MongodbUserSetup mongodbUserSetup, StructureInformation structureInfo, AuthFileWriter authWriter, KeyringWriter keyringWriter) {
         this.mongodbUserSetup = mongodbUserSetup;
         this.userSetup = userSetup;
         this.structureInfo = structureInfo;
-        this.paths = paths;
-        this.creator = creator;
-        this.keyring = keyring;
-        this.prefs = prefs;
+        this.authWriter = authWriter;
+        this.keyringWriter = keyringWriter;
     }
 
     public void createMongodbUser(String username, char[] password) {
@@ -108,10 +92,7 @@
                                               },
                                     "Client admin user username => role assignment."
         );
-        // Hold on to these credentials so that they can be written to keyring
-        // on commit(). This makes gui work out of the box after setup has run.
-        this.clientUsername = username;
-        this.clientPassword = password;
+        keyringWriter.setCredentials(username, password);
     }
 
     public void createAgentUser(String username, char[] password) {
@@ -125,54 +106,17 @@
                                         UserRoles.GRANT_FILES_WRITE_ALL // Agent needs perm to write files.
                                     },
                                     "Agent user username => role assignment.");
-        // Hold on to creds for persistency later.
-        agentUserName = username;
-        agentPassword = password;
+        authWriter.setCredentials(username, password);
     }
 
     @Override
     public void commit() throws IOException {
-        // FIXME: report errors
         mongodbUserSetup.commit();
-        userSetup.commit();
-        writeAgentAuthFile();
-        storeClientCredsToKeyring();
-    }
-    
-    private void storeClientCredsToKeyring() throws IOException {
-        Objects.requireNonNull(clientUsername);
-        Objects.requireNonNull(clientPassword);
-        try {
-            prefs.setSaveEntitlements(true); // force writing on flush()
-            String url = prefs.getConnectionUrl();
-            prefs.setUserName(clientUsername);
-            // Unconditionally save credentials for the chosen username. If setup
-            // runs again it will overwrite existing credentials.
-            prefs.flush();
-            keyring.savePassword(url, clientUsername, clientPassword);
-        } catch (KeyringException e) {
-            // Don't fail setup if storing to keyring fails. After all this is
-            // for convenience only (so that thermostat gui works out of the box
-            // after setup). If we failed, one would have to have a working
-            // keyring setup which isn't always the case. Think thermostat agent.
-            logger.log(Level.INFO, "Failed to store client credentials to keyring. Usability of client commands might suffer.", e);
-        } finally {
-            Arrays.fill(clientPassword, '\0');
-        }
-    }
-
-    private void writeAgentAuthFile() throws IOException {
-        Objects.requireNonNull(agentPassword);
-        Objects.requireNonNull(agentUserName);
-        Properties credentialProps = new Properties();
-        credentialProps.setProperty("username", agentUserName);
-        credentialProps.setProperty("password", String.valueOf(agentPassword));
-        File credentialsFile = paths.getUserAgentAuthConfigFile();
-        creator.create(credentialsFile);
-        try (FileOutputStream fout = new FileOutputStream(credentialsFile)) {
-            credentialProps.store(fout, "Credentials used for 'thermostat agent' connections.");
-        } finally {
-            Arrays.fill(agentPassword, '\0');
+        if (isWebAppInstalled()) {
+            userSetup.commit();
+            authWriter.write();
+            keyringWriter.setStorageUrl(WEB_STORAGE_URL);
+            keyringWriter.write();
         }
     }
 
@@ -185,9 +129,11 @@
         CredentialsFileCreator creator = new CredentialsFileCreator();
         StampFiles stampFiles = new StampFiles(paths);
         StructureInformation info = new StructureInformation(paths);
-        MongodbUserSetup mongoSetup = new MongodbUserSetup(new UserCredsValidator(), launcher, finder, creator , paths, stampFiles, info);
+        ClientPreferences prefs = new ClientPreferences(paths);
+        KeyringWriter keyringWriter = new KeyringWriter(prefs, keyring);
+        AuthFileWriter authWriter = new AuthFileWriter(paths, creator);
+        MongodbUserSetup mongoSetup = new MongodbUserSetup(new UserCredsValidator(), launcher, finder, creator , paths, stampFiles, info, authWriter, keyringWriter);
         ThermostatUserSetup userSetup = new ThermostatUserSetup(new UserPropertiesFinder(finder), new UserCredsValidator(), creator, stampFiles);
-        ClientPreferences prefs = new ClientPreferences(paths);
-        return new ThermostatSetup(userSetup, mongoSetup, info, paths, creator, keyring, prefs);
+        return new ThermostatSetup(userSetup, mongoSetup, info, authWriter, keyringWriter);
     }
 }
--- a/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/ThermostatUserSetup.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/model/ThermostatUserSetup.java	Mon Sep 07 16:44:52 2015 +0200
@@ -72,7 +72,6 @@
 
     @Override
     public void commit() throws IOException {
-        // FIXME: Don't persist users/role setup if webapp is not installed.
         Map<String, CommentedRolePropertyValue> roleProps = buildRoleProperties();
         Map<String, CommentedCredsPropertyValue> userProps = buildUserProperties();
         writeUsers(userProps);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/AuthFileWriterTest.java	Mon Sep 07 16:44:52 2015 +0200
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.setup.command.internal.model;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.shared.config.CommonPaths;
+
+public class AuthFileWriterTest {
+
+    @Test
+    public void canWriteAuthFile() throws IOException {
+        CommonPaths paths = mock(CommonPaths.class);
+        File mockAgentAuthFile = File.createTempFile("thermostat-test-", getClass().getName());
+        try {
+            when(paths.getUserAgentAuthConfigFile()).thenReturn(mockAgentAuthFile);
+            AuthFileWriter authWriter = new AuthFileWriter(paths, mock(CredentialsFileCreator.class));
+            List<String> contents = Files.readAllLines(mockAgentAuthFile.toPath(), Charset.forName("UTF-8"));
+            assertEquals(0, contents.size());
+            char[] password = new char[] { 't', 'e', 's', 't' };
+            authWriter.setCredentials("damian", password);
+            authWriter.write();
+            contents = Files.readAllLines(mockAgentAuthFile.toPath(), Charset.forName("UTF-8"));
+            assertTrue("username and password must be present", contents.size() > 2);
+            assertTrue("username=damian expected to be found in agent.auth file", contents.contains("username=damian"));
+            assertTrue("password=test expected to be found in agent.auth file", contents.contains("password=test"));
+            assertArrayEquals("Expected password to have been cleared.", new char[] { '\0', '\0', '\0', '\0' }, password);
+        } catch (IOException e) {
+            e.printStackTrace();
+            fail("Did not expect failure on commit()");
+        }    
+        finally {
+            Files.delete(mockAgentAuthFile.toPath());
+        }
+    }
+    
+    @Test(expected=NullPointerException.class)
+    public void notSettingCredsBeforeWriteThrowsNPE() throws IOException {
+        AuthFileWriter authWriter = new AuthFileWriter(mock(CommonPaths.class), mock(CredentialsFileCreator.class));
+        authWriter.write(); // expected creds to be set first
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/KeyringWriterTest.java	Mon Sep 07 16:44:52 2015 +0200
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.setup.command.internal.model;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertArrayEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.common.config.ClientPreferences;
+import com.redhat.thermostat.utils.keyring.Keyring;
+import com.redhat.thermostat.utils.keyring.KeyringException;
+
+public class KeyringWriterTest {
+    
+    @Test
+    public void writeStoresToKeyringSuccess() throws IOException {
+        Keyring keyring = mock(Keyring.class); // well behaved keyring
+        doKeyringTest(keyring);
+    }
+
+    private void doKeyringTest(Keyring keyring) throws IOException {
+        ClientPreferences prefs = mock(ClientPreferences.class);
+        KeyringWriter keyringWriter = new KeyringWriter(prefs, keyring);
+        try {
+            String clientUser = "client-admin";
+            char[] clientPass = new char[] { 't' };
+            String storageUrl = "http://somestorage.example.com";
+            keyringWriter.setCredentials(clientUser, clientPass);
+            keyringWriter.setStorageUrl(storageUrl);
+            keyringWriter.write();
+            verify(keyring).savePassword(storageUrl, clientUser, clientPass);
+            verify(prefs).flush();
+            verify(prefs).setSaveEntitlements(true);
+            verify(prefs).setUserName(clientUser);
+            verify(prefs).setConnectionUrl(storageUrl);
+            assertArrayEquals("Expected password array to have been cleared", new char[] { '\0' }, clientPass);
+        } catch (IOException e) {
+            e.printStackTrace();
+            fail("Did not expect failure on commit()");
+        }
+    }
+    
+    @Test
+    public void keyringStoreFailureIsNonFatal() throws IOException {
+        Keyring keyring = mock(Keyring.class);
+        doThrow(new MockKeyringException("This is a test")).when(keyring).savePassword(any(String.class), any(String.class), any(char[].class));
+        doKeyringTest(keyring);
+    }
+    
+    @Test(expected = NullPointerException.class)
+    public void notSettingCredsBeforeWriteThrowsNPE() throws IOException {
+        KeyringWriter writer = new KeyringWriter(mock(ClientPreferences.class), mock(Keyring.class));
+        writer.setStorageUrl("http://somestorage.example.com");
+        writer.write(); // expected creds to be set first.
+    }
+    
+    @Test(expected = NullPointerException.class)
+    public void notSettingStorageUrlBeforeWriteThrowsNPE() throws IOException {
+        KeyringWriter writer = new KeyringWriter(mock(ClientPreferences.class), mock(Keyring.class));
+        writer.setCredentials("something", new char[] { 'x' }); // doesn't matter, just non-null
+        writer.write(); // expected storage URL to be set first
+    }
+    
+    @SuppressWarnings("serial")
+    private static class MockKeyringException extends KeyringException {
+
+        public MockKeyringException(String string) {
+            super(string);
+        }
+        
+    }
+
+}
--- a/setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/MongodbUserSetupTest.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/MongodbUserSetupTest.java	Mon Sep 07 16:44:52 2015 +0200
@@ -37,16 +37,19 @@
 package com.redhat.thermostat.setup.command.internal.model;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.isA;
+import static org.mockito.Matchers.argThat;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import java.io.File;
@@ -70,6 +73,7 @@
 import com.redhat.thermostat.common.cli.AbstractStateNotifyingCommand;
 import com.redhat.thermostat.common.tools.ApplicationState;
 import com.redhat.thermostat.launcher.Launcher;
+import com.redhat.thermostat.setup.command.internal.cli.CharArrayMatcher;
 import com.redhat.thermostat.shared.config.CommonPaths;
 
 public class MongodbUserSetupTest {
@@ -81,6 +85,8 @@
     private CredentialsFileCreator fileCreator;
     private CommonPaths paths;
     private StructureInformation info;
+    private AuthFileWriter authFileWriter;
+    private KeyringWriter keyringWriter;
     
     @Before
     public void setup() {
@@ -90,7 +96,9 @@
         stampFiles = mock(StampFiles.class);
         info = mock(StructureInformation.class);
         mockLauncher = mock(Launcher.class);
-        mongoSetup = new MongodbUserSetup(new UserCredsValidator(), mockLauncher, finder, fileCreator, paths, stampFiles, info) {
+        authFileWriter = mock(AuthFileWriter.class);
+        keyringWriter = mock(KeyringWriter.class);
+        mongoSetup = new MongodbUserSetup(new UserCredsValidator(), mockLauncher, finder, fileCreator, paths, stampFiles, info, authFileWriter, keyringWriter) {
             @Override
             int runMongo() {
                 //instead of running mongo through ProcessBuilder
@@ -220,7 +228,7 @@
         Path testRoot = TestRootHelper.createTestRootDirectory(getClass().getName());
         when(paths.getSystemThermostatHome()).thenReturn(testRoot.toFile());
         try {
-            mongoSetup = new MongodbUserSetup(new UserCredsValidator(), mockLauncher, finder, fileCreator, paths, stampFiles, info) {
+            mongoSetup = new MongodbUserSetup(new UserCredsValidator(), mockLauncher, finder, fileCreator, paths, stampFiles, info, authFileWriter, keyringWriter) {
                 @Override
                 int runMongo() {
                     //return non-zero val to test failure
@@ -276,7 +284,7 @@
     
     @SuppressWarnings("unchecked")
     @Test
-    public void testSetupMongodbUser() throws IOException {
+    public void testSetupMongodbUserWebappInstalled() throws IOException {
         Path testRoot = TestRootHelper.createTestRootDirectory(getClass().getName());
         when(paths.getSystemThermostatHome()).thenReturn(testRoot.toFile());
         try {
@@ -332,7 +340,7 @@
                 }
             }).when(mockLauncher).run(eq(MongodbUserSetup.STORAGE_STOP_ARGS), isA(Collection.class), anyBoolean());
             
-            mongoSetup = new MongodbUserSetup(new UserCredsValidator(), mockLauncher, finder, fileCreator, paths, stampFiles, info) {
+            mongoSetup = new MongodbUserSetup(new UserCredsValidator(), mockLauncher, finder, fileCreator, paths, stampFiles, info, authFileWriter, keyringWriter) {
                 @Override
                 int runMongo() {
                     //instead of running mongo through ProcessBuilder
@@ -358,6 +366,9 @@
             verify(stampFiles).createMongodbUserStamp();
             // temp unlocking calls this
             verify(stampFiles, times(1)).createSetupCompleteStamp(any(String.class));
+            // we don't expect any interactions for the non-webapp case
+            verifyNoMoreInteractions(authFileWriter);
+            verifyNoMoreInteractions(keyringWriter);
     
             assertTrue(mockWebAuthFile.exists());
             Properties webauthProps = new Properties();
@@ -373,10 +384,103 @@
         }
     }
     
-    private void assertArrayEquals(char[] expected, char[] actual) {
-        assertTrue(expected.length == actual.length);
-        for (int i = 0; i < expected.length; i++) {
-            assertEquals(expected[i], actual[i]);
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testSetupMongodbUserNoWebappInstalled() throws IOException {
+        Path testRoot = TestRootHelper.createTestRootDirectory(getClass().getName());
+        Path libDir = Paths.get(testRoot.toString(), "libs");
+        Files.createDirectory(libDir);
+        File createUserJsFile = new File(libDir.toFile(), "create-user.js");
+        createUserJsFile.createNewFile();
+        when(paths.getSystemThermostatHome()).thenReturn(testRoot.toFile());
+        // Fake webapp is *not* installed.
+        when(info.isWebAppInstalled()).thenReturn(false);
+        try {
+            File userDoneFile = File.createTempFile("thermostat", getClass().getName());
+            userDoneFile.deleteOnExit();
+            File setupCompleteFile = File.createTempFile("thermostat", getClass().getName());
+            setupCompleteFile.deleteOnExit();
+            when(stampFiles.getMongodbStampFile()).thenReturn(userDoneFile);
+            when(stampFiles.getSetupCompleteStampFile()).thenReturn(setupCompleteFile);
+    
+            final ActionEvent<ApplicationState> mockActionEvent = mock(ActionEvent.class);
+            AbstractStateNotifyingCommand mockStorage = mock(AbstractStateNotifyingCommand.class);
+            when(mockActionEvent.getSource()).thenReturn(mockStorage);
+            when(mockStorage.getNotifier()).thenReturn(mock(ActionNotifier.class));
+            final Collection<ActionListener<ApplicationState>> listeners[] = new Collection[1];
+            doAnswer(new Answer<Void>() {
+                @Override
+                public Void answer(InvocationOnMock invocation) throws Throwable {
+                    Object[] args = invocation.getArguments();
+                    listeners[0] = (Collection<ActionListener<ApplicationState>>) args[1];
+    
+                    when(mockActionEvent.getActionId()).thenReturn(ApplicationState.START);
+    
+                    for (ActionListener<ApplicationState> listener : listeners[0]) {
+                        listener.actionPerformed(mockActionEvent);
+                    }
+                    return null;
+                }
+            }).when(mockLauncher).run(eq(MongodbUserSetup.STORAGE_START_ARGS), isA(Collection.class), anyBoolean());
+
+            // We started storage successfully, thus after we are done we
+            // stop it again. Mock the storage --stop.
+            doAnswer(new Answer<Void>() {
+                @Override
+                public Void answer(InvocationOnMock invocation) throws Throwable {
+                    Object[] args = invocation.getArguments();
+                    listeners[0] = (Collection<ActionListener<ApplicationState>>) args[1];
+    
+                    when(mockActionEvent.getActionId()).thenReturn(ApplicationState.STOP);
+    
+                    for (ActionListener<ApplicationState> listener : listeners[0]) {
+                        listener.actionPerformed(mockActionEvent);
+                    }
+                    return null;
+                }
+            }).when(mockLauncher).run(eq(MongodbUserSetup.STORAGE_STOP_ARGS), isA(Collection.class), anyBoolean());
+            
+            mongoSetup = new MongodbUserSetup(new UserCredsValidator(), mockLauncher, finder, fileCreator, paths, stampFiles, info, authFileWriter, keyringWriter) {
+                @Override
+                int runMongo() {
+                    //instead of running mongo through ProcessBuilder
+                    //we need to always return 0 for success in tests
+                    return 0;
+                }
+            };
+            String username = "foo-user";
+            char[] password = new char[] { 't', 'e', 's', 't' };
+            try {
+                mongoSetup.createUser(username, password, "bar comment");
+                mongoSetup.commit();
+                // pass
+            } catch (IOException e) {
+                e.printStackTrace();
+                fail("did not expect exception");
+            }
+    
+            verify(mockLauncher, times(1)).run(eq(MongodbUserSetup.STORAGE_START_ARGS), isA(Collection.class), anyBoolean());
+            verify(mockLauncher, times(1)).run(eq(MongodbUserSetup.STORAGE_STOP_ARGS), isA(Collection.class), anyBoolean());
+            verify(mockActionEvent, times(2)).getActionId();
+            verifyNoMoreInteractions(fileCreator); // We don't want web.auth created
+            verify(stampFiles).createMongodbUserStamp();
+            
+            // twice = 1 x temp unlock, 2 x final creation since webapp is not installed
+            verify(stampFiles, times(2)).createSetupCompleteStamp(any(String.class));
+            verify(authFileWriter).setCredentials(eq(username), argThat(matchesPassword(new char[] { 't', 'e', 's', 't' })));
+            verify(authFileWriter).write();
+            verify(keyringWriter).setCredentials(eq(username), argThat(matchesPassword(new char[] { 't', 'e', 's', 't' })));
+            verify(keyringWriter).setStorageUrl(ThermostatSetup.MONGODB_STORAGE_URL);
+            verify(keyringWriter).write();
+    
+            // Passed in password array is expected to be cleared.
+            assertArrayEquals(new char[] { '\0', '\0', '\0', '\0'}, password);
+        } finally {
+            TestRootHelper.recursivelyRemoveTestRootDirectory(testRoot);
         }
     }
+    
+    private CharArrayMatcher matchesPassword(char[] expected) {
+        return new CharArrayMatcher(expected);
+    }
 }
--- a/setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/ThermostatSetupTest.java	Fri Sep 04 14:57:14 2015 -0400
+++ b/setup/command/src/test/java/com/redhat/thermostat/setup/command/internal/model/ThermostatSetupTest.java	Mon Sep 07 16:44:52 2015 +0200
@@ -36,21 +36,17 @@
 
 package com.redhat.thermostat.setup.command.internal.model;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.argThat;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.doThrow;
 
-import java.io.File;
 import java.io.IOException;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Objects;
 
 import org.hamcrest.BaseMatcher;
@@ -58,10 +54,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import com.redhat.thermostat.common.config.ClientPreferences;
-import com.redhat.thermostat.shared.config.CommonPaths;
-import com.redhat.thermostat.utils.keyring.Keyring;
-import com.redhat.thermostat.utils.keyring.KeyringException;
+import com.redhat.thermostat.setup.command.internal.cli.CharArrayMatcher;
 
 public class ThermostatSetupTest {
 
@@ -77,7 +70,7 @@
     @Test
     public void testIsWebAppInstalledDelegates() {
         StructureInformation structureInfo = mock(StructureInformation.class);
-        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, structureInfo, mock(CommonPaths.class), mock(CredentialsFileCreator.class), mock(Keyring.class), mock(ClientPreferences.class));
+        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, structureInfo, mock(AuthFileWriter.class), mock(KeyringWriter.class));
         when(structureInfo.isWebAppInstalled()).thenReturn(true);
         assertTrue(setup.isWebAppInstalled());
         verify(structureInfo).isWebAppInstalled();
@@ -85,76 +78,57 @@
     
     @Test
     public void testCreateAgentUser() {
-        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, mock(StructureInformation.class), mock(CommonPaths.class), mock(CredentialsFileCreator.class), mock(Keyring.class), mock(ClientPreferences.class));
+        AuthFileWriter writer = mock(AuthFileWriter.class);
+        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, mock(StructureInformation.class), writer, mock(KeyringWriter.class));
         setup.createAgentUser("foo-agent", new char[] { 't' });
         verify(userSetup).createRecursiveRole(eq("thermostat-agent"), argThat(new RoleMatcher(UserRoles.AGENT_ROLES)), any(String.class));
         verify(userSetup).assignRolesToUser(eq("foo-agent"), argThat(new RoleMatcher(new String[] { "thermostat-agent", UserRoles.GRANT_FILES_WRITE_ALL })), any(String.class));
+        verify(writer).setCredentials(eq("foo-agent"), argThat(matchesPassword(new char[] { 't' })));
+    }
+    
+    private CharArrayMatcher matchesPassword(char[] password) {
+        return new CharArrayMatcher(password);
     }
     
     @Test
     public void testCreateClientAdminUser() {
-        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, mock(StructureInformation.class), mock(CommonPaths.class), mock(CredentialsFileCreator.class), mock(Keyring.class), mock(ClientPreferences.class));
+        KeyringWriter writer = mock(KeyringWriter.class);
+        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, mock(StructureInformation.class), mock(AuthFileWriter.class), writer);
         setup.createClientAdminUser("foo-client", new char[] { 't' });
         verify(userSetup).createRecursiveRole(eq("thermostat-client"), argThat(new RoleMatcher(UserRoles.CLIENT_ROLES)), any(String.class));
         verify(userSetup).createRecursiveRole(eq("thermostat-cmdc"), argThat(new RoleMatcher(UserRoles.CMD_CHANNEL_GRANT_ALL_ACTIONS)), any(String.class));
         verify(userSetup).createRecursiveRole(eq("thermostat-admin-read-all"), argThat(new RoleMatcher(UserRoles.ADMIN_READALL)), any(String.class));
         String[] clientAllRoles = new String[] { "thermostat-client", "thermostat-cmdc", "thermostat-admin-read-all", UserRoles.PURGE };
         verify(userSetup).assignRolesToUser(eq("foo-client"), argThat(new RoleMatcher(clientAllRoles)), any(String.class));
+        verify(writer).setCredentials(eq("foo-client"), argThat(matchesPassword(new char[] { 't' })));
     }
     
     @Test
-    public void commitCreatesAgentAuthFileStoresToKeyring() throws IOException {
-        Keyring keyring = mock(Keyring.class); // well behaved keyring
-        doCommitTest(keyring);
-    }
-
-    private void doCommitTest(Keyring keyring) throws IOException {
-        ClientPreferences prefs = mock(ClientPreferences.class);
-        CommonPaths paths = mock(CommonPaths.class);
-        File mockAgentAuthFile = File.createTempFile("thermostat-test-", getClass().getName());
-        try {
-            when(paths.getUserAgentAuthConfigFile()).thenReturn(mockAgentAuthFile);
-            ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, mock(StructureInformation.class), paths, mock(CredentialsFileCreator.class), keyring, prefs);
-            List<String> contents = Files.readAllLines(mockAgentAuthFile.toPath(), Charset.forName("UTF-8"));
-            assertEquals(0, contents.size());
-            setup.createAgentUser("damian", new char[] { 't', 'e', 's', 't' });
-            String clientUser = "client-admin";
-            char[] clientPass = new char[] { 't' };
-            setup.createClientAdminUser(clientUser, clientPass);
-            setup.commit();
-            verify(userSetup).commit();
-            verify(mongoUserSetup).commit();
-            verify(keyring).savePassword(prefs.getConnectionUrl(), clientUser, clientPass);
-            verify(prefs).flush();
-            verify(prefs).setSaveEntitlements(true);
-            verify(prefs).setUserName(clientUser);
-            contents = Files.readAllLines(mockAgentAuthFile.toPath(), Charset.forName("UTF-8"));
-            assertTrue("username and password must be present", contents.size() > 2);
-            assertTrue("username=damian expected to be found in agent.auth file", contents.contains("username=damian"));
-            assertTrue("password=test expected to be found in agent.auth file", contents.contains("password=test"));
-        } catch (IOException e) {
-            e.printStackTrace();
-            fail("Did not expect failure on commit()");
-        }    
-        finally {
-            Files.delete(mockAgentAuthFile.toPath());
-        }
+    public void commitCreatesAgentAuthFileStoresToKeyringWhenWebappInstalled() throws IOException {
+        StructureInformation info = mock(StructureInformation.class);
+        when(info.isWebAppInstalled()).thenReturn(true);
+        AuthFileWriter authWriter = mock(AuthFileWriter.class);
+        KeyringWriter keyringWriter = mock(KeyringWriter.class);
+        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, info, authWriter, keyringWriter);
+        setup.commit();
+        verify(authWriter).write();
+        verify(keyringWriter).write();
+        verify(userSetup).commit();
+        verify(mongoUserSetup).commit();
     }
     
     @Test
-    public void keyringStoreFailureIsNonFatal() throws IOException {
-        Keyring keyring = mock(Keyring.class);
-        doThrow(new MockKeyringException("This is a test")).when(keyring).savePassword(any(String.class), any(String.class), any(char[].class));
-        doCommitTest(keyring);
-    }
-    
-    @SuppressWarnings("serial")
-    private static class MockKeyringException extends KeyringException {
-
-        public MockKeyringException(String string) {
-            super(string);
-        }
-        
+    public void commitOnlyCommitsMongodbCredsWhenWebappIsNotInstalled() throws IOException {
+        StructureInformation info = mock(StructureInformation.class);
+        when(info.isWebAppInstalled()).thenReturn(false);
+        AuthFileWriter authWriter = mock(AuthFileWriter.class);
+        KeyringWriter keyringWriter = mock(KeyringWriter.class);
+        ThermostatSetup setup = new ThermostatSetup(userSetup, mongoUserSetup, info, authWriter, keyringWriter);
+        setup.commit();
+        verify(mongoUserSetup).commit();
+        verifyNoMoreInteractions(authWriter);
+        verifyNoMoreInteractions(keyringWriter);
+        verifyNoMoreInteractions(userSetup);
     }
     
     private static class RoleMatcher extends BaseMatcher<String[]> {