From ed613701ae2e7a35076b14a40948d15d74affeb2 Mon Sep 17 00:00:00 2001 From: irengrig Date: Tue, 3 Aug 2010 16:55:14 +0400 Subject: [PATCH] SVN: correct authentication fix. SVN authentication tests --- .../intellij/openapi/vcs/ConcurrentTasks.java | 13 +- .../idea/svn/SvnAuthenticationManager.java | 331 ++++-- .../jetbrains/idea/svn/SvnConfiguration.java | 11 +- .../jetbrains/idea/svn/auth/ProviderType.java | 23 + .../auth/SvnAuthenticationInteraction.java | 29 + .../svn/auth/SvnAuthenticationListener.java | 25 + .../dialogs/SvnAuthenticationProvider.java | 7 +- .../SvnInteractiveAuthenticationProvider.java | 6 +- .../browserCache/RepositoryLoader.java | 22 +- .../idea/svn/SvnAuthenticationTest.java | 1039 +++++++++++++++++ 10 files changed, 1375 insertions(+), 131 deletions(-) create mode 100644 plugins/svn4idea/src/org/jetbrains/idea/svn/auth/ProviderType.java create mode 100644 plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationInteraction.java create mode 100644 plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationListener.java create mode 100644 plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnAuthenticationTest.java diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/ConcurrentTasks.java b/platform/vcs-api/src/com/intellij/openapi/vcs/ConcurrentTasks.java index 8ad878e809e7..cbded4673733 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/ConcurrentTasks.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/ConcurrentTasks.java @@ -22,6 +22,7 @@ import com.intellij.openapi.progress.ProgressManager; import com.intellij.util.Consumer; import com.intellij.util.concurrency.Semaphore; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Future; @@ -33,7 +34,7 @@ public class ConcurrentTasks { private volatile T myResult; private volatile int myCntAlive; private final ProgressIndicator myParentIndicator; - private final Consumer>[] myTasks; + private final List>> myTasks; public void compute() { final EmptyProgressIndicator pi = new EmptyProgressIndicator() { @@ -45,7 +46,7 @@ public class ConcurrentTasks { super.checkCanceled(); } }; - myCntAlive = myTasks.length; + myCntAlive = myTasks.size(); mySemaphore.down(); final List> futures = new LinkedList>(); @@ -85,7 +86,7 @@ public class ConcurrentTasks { if (myResultKnown) break; if (myCntAlive <= 0) break; pi.checkCanceled(); - mySemaphore.waitFor(1000); + mySemaphore.waitFor(300); } // in it possible to even interrupt() threads involved, but at the moment it's better for tasks themselves to check cancel status for (Future future : futures) { @@ -104,6 +105,12 @@ public class ConcurrentTasks { } public ConcurrentTasks(final ProgressIndicator parentIndicator, final Consumer>... tasks) { + myParentIndicator = parentIndicator; + myTasks = Arrays.asList(tasks); + mySemaphore = new Semaphore(); + } + + public ConcurrentTasks(final ProgressIndicator parentIndicator, final List>> tasks) { myParentIndicator = parentIndicator; myTasks = tasks; mySemaphore = new Semaphore(); diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnAuthenticationManager.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnAuthenticationManager.java index 937be282dd2b..98616184b325 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnAuthenticationManager.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnAuthenticationManager.java @@ -16,17 +16,25 @@ package org.jetbrains.idea.svn; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Getter; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.CalledInAwt; +import com.intellij.openapi.vcs.changes.committed.AbstractCalledLater; import com.intellij.openapi.vcs.changes.ui.ChangesViewBalloonProblemNotifier; -import com.intellij.ui.GuiUtils; -import com.intellij.util.containers.SoftHashMap; +import com.intellij.util.EventDispatcher; import com.intellij.util.net.HttpConfigurable; import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.svn.auth.ProviderType; +import org.jetbrains.idea.svn.auth.SvnAuthenticationInteraction; +import org.jetbrains.idea.svn.auth.SvnAuthenticationListener; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; @@ -37,43 +45,74 @@ import org.tmatesoft.svn.core.io.SVNRepository; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.Iterator; -import java.util.Map; -import java.util.StringTokenizer; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.*; /** * @author alex */ -public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager { +public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager implements SvnAuthenticationListener { private static final Logger LOG = Logger.getInstance(SvnAuthenticationManager.class.getName()); private final Project myProject; private final File myConfigDirectory; private PersistentAuthenticationProviderProxy myPersistentAuthenticationProviderProxy; private SvnConfiguration myConfig; - private AtomicBoolean myStoreInPlainTextAnyway = new AtomicBoolean(false); + // instead of ThreadLocal + private final Set myPlainTextAllowed; + private static final ThreadLocal ourJustEntered = new ThreadLocal(); + private SvnAuthenticationInteraction myInteraction; + private final EventDispatcher myListener; public SvnAuthenticationManager(final Project project, final File configDirectory) { super(configDirectory, true, null, null); myProject = project; myConfigDirectory = configDirectory; + myListener = EventDispatcher.create(SvnAuthenticationListener.class); myConfig = SvnConfiguration.getInstance(myProject); if (myPersistentAuthenticationProviderProxy != null) { myPersistentAuthenticationProviderProxy.setProject(myProject); } + myPlainTextAllowed = Collections.synchronizedSet(new HashSet()); + myInteraction = new MySvnAuthenticationInteraction(myProject); + } + + public void addListener(final SvnAuthenticationListener listener) { + myListener.addListener(listener); + } + + @Override + public void actualSaveWillBeTried(ProviderType type, SVNURL url, String realm, String kind) { + myListener.getMulticaster().actualSaveWillBeTried(type, url, realm, kind); + } + + @Override + public void requested(ProviderType type, SVNURL url, String realm, String kind, boolean canceled) { + if (ProviderType.interactive.equals(type) && (! canceled)) { + ourJustEntered.set(true); + } + myListener.getMulticaster().requested(type, url, realm, kind, canceled); } @Override protected ISVNAuthenticationProvider createCacheAuthenticationProvider(File authDir, String userName) { - myStoreInPlainTextAnyway = new AtomicBoolean(false); myPersistentAuthenticationProviderProxy = new PersistentAuthenticationProviderProxy(super.createCacheAuthenticationProvider(authDir, userName), authDir); return myPersistentAuthenticationProviderProxy; } + @Override + public void acknowledgeAuthentication(boolean accepted, + String kind, + String realm, + SVNErrorMessage errorMessage, + SVNAuthentication authentication) throws SVNException { + try { + super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication); + } finally { + final Thread currentThread = Thread.currentThread(); + myPlainTextAllowed.remove(currentThread); + } + } + private class PersistentAuthenticationProviderProxy implements ISVNAuthenticationProvider, IPersistentAuthenticationProvider { - private final Map myRewritePreventer; - private static final long ourRefreshInterval = 6000 * 1000; private final ISVNAuthenticationProvider myDelegate; private final File myAuthDir; private Project myProject; @@ -81,7 +120,6 @@ public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager { private PersistentAuthenticationProviderProxy(final ISVNAuthenticationProvider delegate, final File authDir) { myDelegate = delegate; myAuthDir = authDir; - myRewritePreventer = new SoftHashMap(); } public void setProject(Project project) { @@ -89,9 +127,11 @@ public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager { } public SVNAuthentication requestClientAuthentication(final String kind, final SVNURL url, final String realm, final SVNErrorMessage errorMessage, - final SVNAuthentication previousAuth, - final boolean authMayBeStored) { - return myDelegate.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, false); + final SVNAuthentication previousAuth, final boolean authMayBeStored) { + final SVNAuthentication svnAuthentication = + myDelegate.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored); + myListener.getMulticaster().requested(ProviderType.persistent, url, realm, kind, svnAuthentication == null); + return svnAuthentication; } public int acceptServerAuthentication(final SVNURL url, final String realm, final Object certificate, final boolean resultMayBeStored) { @@ -99,44 +139,37 @@ public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager { } public void saveAuthentication(final SVNAuthentication auth, final String kind, final String realm) throws SVNException { - try { - final SvnAuthWrapperEqualable newKey = new SvnAuthWrapperEqualable(auth); - final Long recent = myRewritePreventer.get(newKey); - final long currTime = System.currentTimeMillis(); - File dir = new File(myAuthDir, kind); - String fileName = SVNFileUtil.computeChecksum(realm); - File authFile = new File(dir, fileName); + final Boolean fromInteractive = ourJustEntered.get(); + ourJustEntered.set(null); + if (! Boolean.TRUE.equals(fromInteractive)) { + // not what user entered + return; + } - if ((! authFile.exists()) || recent == null || ((recent != null) && ((currTime - recent.longValue()) > ourRefreshInterval))) { + if ((!auth.isStorageAllowed()) || (USERNAME.equals(kind))) return; - if (auth.isStorageAllowed() && ISVNAuthenticationManager.USERNAME != kind) { - try { - GuiUtils.runOrInvokeAndWait(new Runnable() { - public void run() { - checkContinueSaveCredentials(auth, kind, realm); + saveCredentialsIfAllowed(auth, kind, realm, new Runnable() { + @Override + public void run() { + File dir = new File(myAuthDir, kind); + String fileName = SVNFileUtil.computeChecksum(realm); + File authFile = new File(dir, fileName); + + myListener.getMulticaster().actualSaveWillBeTried(ProviderType.persistent, auth.getURL(), realm, kind); + try { + ((IPersistentAuthenticationProvider) myDelegate).saveAuthentication(auth, kind, realm); + } + catch (SVNException e) { + if (myProject != null) { + ApplicationManager.getApplication().invokeLater(new ChangesViewBalloonProblemNotifier(myProject, + "Problem when storing Subversion credentials: " + e.getMessage(), MessageType.ERROR)); } - }); - } catch (InvocationTargetException e) { - LOG.error(e); - } catch (InterruptedException e) { - LOG.error(e); + } finally { + // do not make password file readonly + setWriteable(authFile); + } } - } - - ((DefaultSVNAuthenticationManager.IPersistentAuthenticationProvider)myDelegate).saveAuthentication(auth, kind, realm); - - // do not make password file readonly - setWriteable(authFile); - - myRewritePreventer.put(newKey, currTime); - } - } - catch (final SVNException e) { - // show notification so that user was aware his credentials were not saved - if (myProject == null) return; - ApplicationManager.getApplication().invokeLater(new ChangesViewBalloonProblemNotifier(myProject, - "Problem when storing Subversion credentials: " + e.getMessage(), MessageType.ERROR)); - } + }); } private final static int maxAttempts = 10; @@ -163,13 +196,6 @@ public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager { } } - public boolean haveStoredCredentials(final String kind, final SVNURL url, final String realm, final SVNErrorMessage errorMessage, - final SVNAuthentication previousAuth) { - final SVNAuthentication result = - myPersistentAuthenticationProviderProxy.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, false); - return result != null; - } - public ISVNProxyManager getProxyManager(SVNURL url) throws SVNException { // this code taken from default manager (changed for system properties reading) String host = url.getHost(); @@ -358,27 +384,6 @@ public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager { return null; } - private static class SvnAuthWrapperEqualable extends Wrapper { - private SvnAuthWrapperEqualable(SVNAuthentication svnAuthentication) { - super(svnAuthentication); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (obj instanceof SvnAuthWrapperEqualable) { - return SvnAuthEquals.equals(this.getT(), ((SvnAuthWrapperEqualable) obj).getT()); - } - return false; - } - - @Override - public int hashCode() { - return SvnAuthEquals.hashCode(getT()); - } - } - private void setPropertyForHost(final String host, final String property, final String value) { final SVNConfigFile userConfig = new SVNConfigFile(new File(myConfigDirectory, "servers")); @@ -418,84 +423,176 @@ public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager { return "yes".equalsIgnoreCase(storeAuthCreds) || "on".equalsIgnoreCase(storeAuthCreds) || "true".equalsIgnoreCase(storeAuthCreds); } - public boolean checkContinueSaveCredentials(final SVNAuthentication auth, final String kind, final String realm) { + public void saveCredentialsIfAllowed(final SVNAuthentication auth, final String kind, final String realm, + final Runnable saveRunnable) { final SVNURL url = auth.getURL(); final String storeCredentials = getConfigFile().getPropertyValue("auth", "store-auth-creds"); if ((Boolean.FALSE.equals(isAuthStorageEnabledMy(url))) || (! isTurned(storeCredentials))) { - ChangesViewBalloonProblemNotifier.showMe(myProject, "Cannot store credentials: forbidden by \"store-auth-creds=no\"", MessageType.ERROR); - return false; + myInteraction.warnOnAuthStorageDisabled(url); + return; } final boolean passwordStorageEnabled = isStorePasswords(url); // check can store if ((! ISVNAuthenticationManager.SSL.equals(kind)) && (! passwordStorageEnabled)) { // but it should be - //userConfig.setPropertyValue("auth", "store-passwords", "yes", true); - ChangesViewBalloonProblemNotifier.showMe(myProject, "Cannot store password: forbidden by \"store-passwords=no\"", MessageType.ERROR); - return false; + myInteraction.warnOnPasswordStorageDisabled(url); + return; } if (ISVNAuthenticationManager.SSL.equals(kind) && (! isStoreSSLClientCertificatePassphrases(url))) { - //setPropertyForHost(url.getHost(), "store-ssl-client-cert-pp", "yes"); - ChangesViewBalloonProblemNotifier.showMe(myProject, "Cannot store passphrase: forbidden by \"store-ssl-client-cert-pp=no\"", MessageType.ERROR); - return false; + myInteraction.warnOnSSLPassphraseStorageDisabled(url); + return; } // check can encrypt if (!(SystemInfo.isWindows && SVNJNAUtil.isWinCryptEnabled())) { - if (ISVNAuthenticationManager.SSL.equals(kind)) { - try { + try { + if (ISVNAuthenticationManager.SSL.equals(kind)) { if (!isStorePlainTextPassphrases(realm, auth)) { - return askToStoreUnencrypted("Store the passphrase in plaintext?", - String.format("Your passphrase for client certificate:\n%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?", - ((SVNSSLAuthentication)auth).getCertificateFile().getPath())); + promptAndSaveWhenWeLackEncryption(saveRunnable, + new Getter() { + @Override + public Boolean get() { + return myInteraction.promptForSSLPlaintextPassphraseSaving(url, realm, ((SVNSSLAuthentication)auth).getCertificateFile()); + } + }); + return; } - } catch (SVNException e) { - LOG.error(e); // should not occur, anyway means not allowed - } - } else { - try { + } else { if (!isStorePlainTextPasswords(realm, auth)) { - return askToStoreUnencrypted("Store the password in plaintext?", - String.format("Your password for authentication realm:\n%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?", - realm)); + promptAndSaveWhenWeLackEncryption(saveRunnable, + new Getter() { + @Override + public Boolean get() { + return myInteraction.promptForPlaintextPasswordSaving(url, realm); + } + }); + return; } - } catch (SVNException e) { - LOG.error(e); } + } catch (SVNException e) { + LOG.info(e); + return; } } - return true; + saveRunnable.run(); } @Override protected boolean isStorePlainTextPasswords(String realm, SVNAuthentication auth) throws SVNException { - // normally check user preferences, but the user may override them via the dialog from checkContinueSaveCredentials() - return myStoreInPlainTextAnyway.get() || super.isStorePlainTextPasswords(realm, auth); + return myPlainTextAllowed.contains(Thread.currentThread()) || super.isStorePlainTextPasswords(realm, auth); } @Override protected boolean isStorePlainTextPassphrases(String realm, SVNAuthentication auth) throws SVNException { - return myStoreInPlainTextAnyway.get() || super.isStorePlainTextPassphrases(realm, auth); + return myPlainTextAllowed.contains(Thread.currentThread()) || super.isStorePlainTextPassphrases(realm, auth); + } + + private ModalityState getCurrent() { + if (ApplicationManager.getApplication().isDispatchThread()) { + return ModalityState.current(); + } + final ProgressIndicator pi = ProgressManager.getInstance().getProgressIndicator(); + if (pi == null) { + return ModalityState.defaultModalityState(); + } + return pi.getModalityState(); + } + + private void promptAndSaveWhenWeLackEncryption( + final Runnable saveRunnable, + final Getter prompt) { + + final Runnable actualSave = new Runnable() { + @Override + public void run() { + final Thread currentThread = Thread.currentThread(); + myPlainTextAllowed.add(currentThread); + try { + saveRunnable.run(); + } + finally { + myPlainTextAllowed.remove(currentThread); + } + } + }; + + if (myInteraction.promptInAwt()) { + new AbstractCalledLater(myProject, getCurrent()) { + @Override + public void run() { + final boolean saveOnce = Boolean.TRUE.equals(prompt.get()); + if (saveOnce) { + ApplicationManager.getApplication().executeOnPooledThread(actualSave); + } else { + } + } + }.callMe(); + } else { + final boolean saveOnce = Boolean.TRUE.equals(prompt.get()); + if (saveOnce) { + actualSave.run(); + } + } } /** * Shows a yes/no question whether user wants to store his password in plain text and returns his answer. - * Also updates the 'myStoreInPlainTextAnyway' variable correspondingly. - * This method shuld be called from the event dispatching thread. * @param title title of the questioning dialog. * @param message questioning message to be displayed. * @return true if user agrees to store his password in plaintext, false if he doesn't. */ + @CalledInAwt private boolean askToStoreUnencrypted(String title, String message) { final int answer = Messages.showYesNoDialog(myProject, message, title, Messages.getQuestionIcon()); - if (answer == 0) { - myStoreInPlainTextAnyway.set(true); - } else { - myStoreInPlainTextAnyway.set(false); - ChangesViewBalloonProblemNotifier.showMe(myProject, "Cannot store password", MessageType.ERROR); - } - return myStoreInPlainTextAnyway.get(); - + return answer == 0; } -} \ No newline at end of file + public void setInteraction(SvnAuthenticationInteraction interaction) { + myInteraction = interaction; + } + + private static class MySvnAuthenticationInteraction implements SvnAuthenticationInteraction { + private final Project myProject; + + private MySvnAuthenticationInteraction(Project project) { + myProject = project; + } + + @Override + public void warnOnAuthStorageDisabled(SVNURL url) { + ChangesViewBalloonProblemNotifier.showMe(myProject, "Cannot store credentials: forbidden by \"store-auth-creds=no\"", MessageType.ERROR); + } + + @Override + public void warnOnPasswordStorageDisabled(SVNURL url) { + ChangesViewBalloonProblemNotifier.showMe(myProject, "Cannot store password: forbidden by \"store-passwords=no\"", MessageType.ERROR); + } + + @Override + public void warnOnSSLPassphraseStorageDisabled(SVNURL url) { + ChangesViewBalloonProblemNotifier.showMe(myProject, "Cannot store passphrase: forbidden by \"store-ssl-client-cert-pp=no\"", MessageType.ERROR); + } + + @Override + public boolean promptForPlaintextPasswordSaving(SVNURL url, String realm) { + final int answer = Messages.showYesNoDialog(myProject, String.format("Your password for authentication realm:\n" + + "%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?", realm), + "Store the password in plaintext?", Messages.getQuestionIcon()); + return answer == 0; + } + + @Override + public boolean promptInAwt() { + return true; + } + + @Override + public boolean promptForSSLPlaintextPassphraseSaving(SVNURL url, String realm, File certificateFile) { + final int answer = Messages.showYesNoDialog(myProject, String.format("Your passphrase for client certificate:\n%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?", + certificateFile.getPath()), + "Store the passphrase in plaintext?", Messages.getQuestionIcon()); + return answer == 0; + } + } +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnConfiguration.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnConfiguration.java index d21e7f7f5bd4..9ddaa9ff800e 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnConfiguration.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnConfiguration.java @@ -93,6 +93,7 @@ public class SvnConfiguration implements ProjectComponent, JDOMExternalizable { private final Map myMergeRootInfos = new HashMap(); private final Map myUpdateRootInfos = new HashMap(); private final List myAnnotationListeners; + private SvnInteractiveAuthenticationProvider myInteractiveProvider; public static SvnConfiguration getInstance(Project project) { return project.getComponent(SvnConfiguration.class); @@ -200,6 +201,7 @@ public class SvnConfiguration implements ProjectComponent, JDOMExternalizable { myAuthManager = null; myPassiveAuthManager = null; myInteractiveManager = null; + myInteractiveProvider = null; RUNTIME_AUTH_CACHE.clear(); } @@ -209,6 +211,7 @@ public class SvnConfiguration implements ProjectComponent, JDOMExternalizable { myAuthManager = null; myPassiveAuthManager = null; myInteractiveManager = null; + myInteractiveProvider = null; RUNTIME_AUTH_CACHE.clear(); } @@ -224,8 +227,9 @@ public class SvnConfiguration implements ProjectComponent, JDOMExternalizable { if (myAuthManager == null) { // reloaded when configuration directory changes myAuthManager = new SvnAuthenticationManager(myProject, new File(getConfigurationDirectory())); - myAuthManager.setAuthenticationProvider(new SvnAuthenticationProvider(svnVcs, getInteractiveManager(svnVcs))); - myAuthManager.setRuntimeStorage(RUNTIME_AUTH_CACHE); + // to init + myAuthManager.setAuthenticationProvider(new SvnAuthenticationProvider(svnVcs, myInteractiveProvider)); + myAuthManager.setRuntimeStorage(RUNTIME_AUTH_CACHE); } return myAuthManager; } @@ -242,7 +246,8 @@ public class SvnConfiguration implements ProjectComponent, JDOMExternalizable { if (myInteractiveManager == null) { myInteractiveManager = new SvnAuthenticationManager(myProject, new File(getConfigurationDirectory())); myInteractiveManager.setRuntimeStorage(RUNTIME_AUTH_CACHE); - myInteractiveManager.setAuthenticationProvider(new SvnInteractiveAuthenticationProvider(svnVcs, myInteractiveManager)); + myInteractiveProvider = new SvnInteractiveAuthenticationProvider(svnVcs, myInteractiveManager); + myInteractiveManager.setAuthenticationProvider(myInteractiveProvider); } return myInteractiveManager; } diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/ProviderType.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/ProviderType.java new file mode 100644 index 000000000000..c0cedda1fda4 --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/ProviderType.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.idea.svn.auth; + +public enum ProviderType { + dumb, + interactive, + persistent, + memory_cache +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationInteraction.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationInteraction.java new file mode 100644 index 000000000000..a43f6fad829d --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationInteraction.java @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.idea.svn.auth; + +import org.tmatesoft.svn.core.SVNURL; + +import java.io.File; + +public interface SvnAuthenticationInteraction { + void warnOnAuthStorageDisabled(final SVNURL url); + void warnOnPasswordStorageDisabled(final SVNURL url); + void warnOnSSLPassphraseStorageDisabled(final SVNURL url); + boolean promptForSSLPlaintextPassphraseSaving(final SVNURL url, String realm, File certificateFile); + boolean promptForPlaintextPasswordSaving(final SVNURL url, String realm); + boolean promptInAwt(); +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationListener.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationListener.java new file mode 100644 index 000000000000..b8975b19d3c9 --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/auth/SvnAuthenticationListener.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.idea.svn.auth; + +import org.tmatesoft.svn.core.SVNURL; + +import java.util.EventListener; + +public interface SvnAuthenticationListener extends EventListener { + void requested(final ProviderType type, final SVNURL url, String realm, String kind, boolean canceled); + void actualSaveWillBeTried(final ProviderType type, final SVNURL url, String realm, String kind); +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnAuthenticationProvider.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnAuthenticationProvider.java index 850d8f524f46..51470af6826e 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnAuthenticationProvider.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnAuthenticationProvider.java @@ -20,7 +20,6 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.ui.GuiUtils; import com.intellij.util.SystemProperties; -import org.jetbrains.idea.svn.SvnAuthenticationManager; import org.jetbrains.idea.svn.SvnAuthenticationNotifier; import org.jetbrains.idea.svn.SvnVcs; import org.tmatesoft.svn.core.SVNErrorMessage; @@ -40,12 +39,12 @@ public class SvnAuthenticationProvider implements ISVNAuthenticationProvider { private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.dialogs.SvnAuthenticationProvider"); private final Project myProject; private final SvnAuthenticationNotifier myAuthenticationNotifier; - private final SvnInteractiveAuthenticationProvider mySvnInteractiveAuthenticationProvider; + private final ISVNAuthenticationProvider mySvnInteractiveAuthenticationProvider; - public SvnAuthenticationProvider(final SvnVcs svnVcs, final SvnAuthenticationManager manager) { + public SvnAuthenticationProvider(final SvnVcs svnVcs, final ISVNAuthenticationProvider provider) { myProject = svnVcs.getProject(); myAuthenticationNotifier = svnVcs.getAuthNotifier(); - mySvnInteractiveAuthenticationProvider = new SvnInteractiveAuthenticationProvider(svnVcs, manager); + mySvnInteractiveAuthenticationProvider = provider; } private void log(final String s) { diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnInteractiveAuthenticationProvider.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnInteractiveAuthenticationProvider.java index e26528930dda..376bcc49b037 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnInteractiveAuthenticationProvider.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnInteractiveAuthenticationProvider.java @@ -24,6 +24,7 @@ import org.jetbrains.idea.svn.SvnAuthenticationManager; import org.jetbrains.idea.svn.SvnBundle; import org.jetbrains.idea.svn.SvnConfiguration; import org.jetbrains.idea.svn.SvnVcs; +import org.jetbrains.idea.svn.auth.ProviderType; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.*; @@ -35,11 +36,9 @@ public class SvnInteractiveAuthenticationProvider implements ISVNAuthenticationP private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.dialogs.SvnInteractiveAuthenticationProvider"); private final Project myProject; private static final ThreadLocal myCallState = new ThreadLocal(); - private final SvnVcs myVcs; private final SvnAuthenticationManager myManager; public SvnInteractiveAuthenticationProvider(final SvnVcs vcs, SvnAuthenticationManager manager) { - myVcs = vcs; myManager = manager; myProject = vcs.getProject(); } @@ -176,6 +175,9 @@ public class SvnInteractiveAuthenticationProvider implements ISVNAuthenticationP final boolean wasCanceled = result[0] == null; callState.setWasCancelled(wasCanceled); + if (! ISVNAuthenticationManager.USERNAME.equals(kind)) { + myManager.requested(ProviderType.interactive, url, realm, kind, wasCanceled); + } return result[0]; } diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/browserCache/RepositoryLoader.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/browserCache/RepositoryLoader.java index a40ae967c14a..68da5c966a0a 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/browserCache/RepositoryLoader.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/browserCache/RepositoryLoader.java @@ -16,6 +16,9 @@ package org.jetbrains.idea.svn.dialogs.browserCache; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.progress.EmptyProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.Pair; import org.jetbrains.idea.svn.SvnVcs; import org.jetbrains.idea.svn.dialogs.RepositoryTreeNode; @@ -45,7 +48,7 @@ class RepositoryLoader extends Loader { final Pair data = new Pair(node, afterRefreshExpander); if (! myQueueProcessorActive) { - ApplicationManager.getApplication().executeOnPooledThread(new LoadTask(data)); + startLoadTask(data); myQueueProcessorActive = true; } else { myLoadQueue.offer(data); @@ -74,10 +77,25 @@ class RepositoryLoader extends Loader { // ignore if node is already disposed startNext(); } else { - ApplicationManager.getApplication().executeOnPooledThread(new LoadTask(data)); + startLoadTask(data); } } + private void startLoadTask(final Pair data) { + final ModalityState state = ModalityState.current(); + ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { + @Override + public void run() { + ProgressManager.getInstance().runProcess(new LoadTask(data), new EmptyProgressIndicator() { + @Override + public ModalityState getModalityState() { + return state; + } + }); + } + }); + } + public void forceRefresh(final String repositoryRootUrl) { // ? remove } diff --git a/plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnAuthenticationTest.java b/plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnAuthenticationTest.java new file mode 100644 index 000000000000..93e4ff3716f0 --- /dev/null +++ b/plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnAuthenticationTest.java @@ -0,0 +1,1039 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.idea.svn; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.Trinity; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.util.concurrency.Semaphore; +import junit.framework.Assert; +import org.jetbrains.idea.svn.auth.ProviderType; +import org.jetbrains.idea.svn.auth.SvnAuthenticationInteraction; +import org.jetbrains.idea.svn.auth.SvnAuthenticationListener; +import org.jetbrains.idea.svn.dialogs.SvnAuthenticationProvider; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.auth.*; +import org.tmatesoft.svn.core.internal.util.jna.SVNJNAUtil; +import org.tmatesoft.svn.core.internal.wc.SVNConfigFile; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SvnAuthenticationTest extends PlatformTestCase { + private SvnAuthenticationManager myAuthenticationManager; + private TestInteraction myTestInteraction; + private SvnVcs myVcs; + private final Object mySynchObject = new Object(); + private SvnTestInteractiveAuthentication myInteractiveProvider; + private SvnConfiguration myConfiguration; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + myConfiguration = SvnConfiguration.getInstance(myProject); + final String configPath = myProject.getBaseDir().getPath() + File.separator + "Subversion"; + myConfiguration.setConfigurationDirectory(configPath); + + final File configFile = new File(configPath); + myFilesToDelete.add(configFile); + + myVcs = SvnVcs.getInstance(myProject); + + myAuthenticationManager = new SvnAuthenticationManager(myProject, configFile); + + myInteractiveProvider = new SvnTestInteractiveAuthentication(myAuthenticationManager); + myAuthenticationManager.setAuthenticationProvider(new SvnAuthenticationProvider(myVcs, myInteractiveProvider)); + myAuthenticationManager.setRuntimeStorage(SvnConfiguration.RUNTIME_AUTH_CACHE); + + myTestInteraction = new TestInteraction(); + myAuthenticationManager.setInteraction(myTestInteraction); + + SVNConfigFile.createDefaultConfiguration(configFile); + } + + public void testSavedAndRead() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.save)); + + commonScheme(url, false, null); + Assert.assertEquals(3, listener.getCnt()); + //long start = System.currentTimeMillis(); + //waitListenerStep(start, listener, 3); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + commonScheme(url, false, null); + //start = System.currentTimeMillis(); + //waitListenerStep(start, listener, 4); + Assert.assertEquals(4, listener.getCnt()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + myTestInteraction.assertNothing(); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } + + private void waitListenerStep(long start, TestListener listener, final int stepNoNext) { + while ((listener.getCnt() < stepNoNext) && ((System.currentTimeMillis() - start) < 10000)) { + synchronized (mySynchObject) { + try { + mySynchObject.wait(50); + } + catch (InterruptedException e) { + // + } + } + } + Assert.assertEquals(stepNoNext, listener.getCnt()); + } + + public void testWhenNotSaved() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + myInteractiveProvider.setSaveData(false); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + //long start = System.currentTimeMillis(); + //waitListenerStep(start, listener, 2); + Assert.assertEquals(2, listener.getCnt()); + + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + // cause is not cleared though + savedOnceListener.reset(); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + Assert.assertEquals(4, listener.getCnt()); + //start = System.currentTimeMillis(); + //waitListenerStep(start, listener, 4); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + myTestInteraction.assertNothing(); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } + + public void testWhenAuthCredsNoInConfig() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + final File config = new File(myConfiguration.getConfigurationDirectory(), "config"); + final char[] chars = FileUtil.loadFileText(config); + final String contents = String.valueOf(chars); + final String auth = "[auth]"; + final int idx = contents.indexOf(auth); + Assert.assertTrue(idx != -1); + final String newContents = contents.substring(0, idx + auth.length()) + "\nstore-auth-creds=no\n" + contents.substring(idx + auth.length()); + + final File oldConfig = new File(myConfiguration.getConfigurationDirectory(), "config_old"); + FileUtil.rename(config, oldConfig); + try { + config.createNewFile(); + FileUtil.appendToFile(config, newContents); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + Assert.assertEquals(2, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumAuthWarn()); + myTestInteraction.reset(); + savedOnceListener.assertForAwt(); + savedOnceListener.reset(); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + Assert.assertEquals(4, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumAuthWarn()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumAuthWarn()); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } finally { + FileUtil.delete(config); + FileUtil.rename(oldConfig, config); + } + } + + public void testWhenAuthCredsNoInServers() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + final File servers = new File(myConfiguration.getConfigurationDirectory(), "servers"); + + final File oldServers = new File(myConfiguration.getConfigurationDirectory(), "config_old"); + FileUtil.copy(servers, oldServers); + try { + FileUtil.appendToFile(servers, "\nstore-auth-creds=no\n"); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + Assert.assertEquals(2, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumAuthWarn()); + myTestInteraction.reset(); + savedOnceListener.assertForAwt(); + savedOnceListener.reset(); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + Assert.assertEquals(4, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumAuthWarn()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumAuthWarn()); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } finally { + FileUtil.delete(servers); + FileUtil.rename(oldServers, servers); + } + } + + public void testWhenPassSaveNoInConfig() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + final File config = new File(myConfiguration.getConfigurationDirectory(), "config"); + final char[] chars = FileUtil.loadFileText(config); + final String contents = String.valueOf(chars); + final String auth = "[auth]"; + final int idx = contents.indexOf(auth); + Assert.assertTrue(idx != -1); + final String newContents = contents.substring(0, idx + auth.length()) + "\nstore-passwords=no\n" + contents.substring(idx + auth.length()); + + final File oldConfig = new File(myConfiguration.getConfigurationDirectory(), "config_old"); + FileUtil.rename(config, oldConfig); + try { + config.createNewFile(); + FileUtil.appendToFile(config, newContents); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + Assert.assertEquals(2, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + myTestInteraction.reset(); + savedOnceListener.assertForAwt(); + savedOnceListener.reset(); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + Assert.assertEquals(4, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } finally { + FileUtil.delete(config); + FileUtil.rename(oldConfig, config); + } + } + + public void testWhenPassSaveNoInServers() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + final File servers = new File(myConfiguration.getConfigurationDirectory(), "servers"); + + final File oldServers = new File(myConfiguration.getConfigurationDirectory(), "config_old"); + FileUtil.copy(servers, oldServers); + try { + FileUtil.appendToFile(servers, "\nstore-passwords=no\n"); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + Assert.assertEquals(2, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + myTestInteraction.reset(); + savedOnceListener.assertForAwt(); + savedOnceListener.reset(); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + Assert.assertEquals(4, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } finally { + FileUtil.delete(servers); + FileUtil.rename(oldServers, servers); + } + } + + public void testWhenPassSaveNoForGroup() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + final File servers = new File(myConfiguration.getConfigurationDirectory(), "servers"); + final char[] chars = FileUtil.loadFileText(servers); + final String contents = String.valueOf(chars); + final String groups = "[groups]"; + final int idx = contents.indexOf(groups); + Assert.assertTrue(idx != -1); + final String newContents = contents.substring(0, idx + groups.length()) + "\nsomegroup=some*\n" + contents.substring(idx + groups.length()) + + "\n[somegroup]\nstore-passwords=no\n"; + + final File oldServers = new File(myConfiguration.getConfigurationDirectory(), "config_old"); + FileUtil.rename(servers, oldServers); + try { + servers.createNewFile(); + FileUtil.appendToFile(servers, newContents); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + Assert.assertEquals(2, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + myTestInteraction.reset(); + savedOnceListener.assertForAwt(); + savedOnceListener.reset(); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + Assert.assertEquals(4, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumPasswordsWarn()); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } finally { + FileUtil.delete(servers); + FileUtil.rename(oldServers, servers); + } + } + + public void testWhenPassPhraseSaveNo() throws Exception { + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + + final File servers = new File(myConfiguration.getConfigurationDirectory(), "servers"); + + final File oldServers = new File(myConfiguration.getConfigurationDirectory(), "config_old"); + FileUtil.copy(servers, oldServers); + try { + FileUtil.appendToFile(servers, "\nstore-ssl-client-cert-pp=no\n"); + + final SVNURL url = SVNURL.parseURIEncoded("https://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + Assert.assertEquals(2, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumSSLWarn()); + myTestInteraction.reset(); + savedOnceListener.assertForAwt(); + savedOnceListener.reset(); + + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + Assert.assertEquals(4, listener.getCnt()); + Assert.assertEquals(1, myTestInteraction.getNumSSLWarn()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumSSLWarn()); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + } finally { + FileUtil.delete(servers); + FileUtil.rename(oldServers, servers); + } + } + + public void testPlaintextPrompt() throws Exception { + SVNJNAUtil.setJNAEnabled(false); + + // yes, no + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + myTestInteraction.setPlaintextAnswer(true); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.save)); + + commonScheme(url, false, null); + long start = System.currentTimeMillis(); + waitListenerStep(start, listener, 3); + Assert.assertEquals(1, myTestInteraction.getNumPlaintextPrompt()); + savedOnceListener.assertSaved(url, ISVNAuthenticationManager.PASSWORD); + savedOnceListener.reset(); + myTestInteraction.reset(); + + myConfiguration.clearAuthenticationDirectory(); + myTestInteraction.setPlaintextAnswer(false); + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + start = System.currentTimeMillis(); + waitListenerStep(start, listener, 5); + Assert.assertEquals(1, myTestInteraction.getNumPlaintextPrompt()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumPlaintextPrompt()); + Assert.assertEquals(5, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + SVNJNAUtil.setJNAEnabled(true); + } + + public void testPlaintextPromptAndSecondPrompt() throws Exception { + SVNJNAUtil.setJNAEnabled(false); + + // yes, no + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + myTestInteraction.setPlaintextAnswer(false); + + final SVNURL url = SVNURL.parseURIEncoded("http://some.host.com/repo"); + final SVNURL url2 = SVNURL.parseURIEncoded("http://some.other.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + + commonScheme(url, false, null); + long start = System.currentTimeMillis(); + waitListenerStep(start, listener, 2); + Assert.assertEquals(1, myTestInteraction.getNumPlaintextPrompt()); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + savedOnceListener.reset(); + myTestInteraction.reset(); + + listener.addStep(new Trinity(ProviderType.persistent, url2, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url2, Type.request)); + commonScheme(url2, false, "anotherRealm"); + start = System.currentTimeMillis(); + waitListenerStep(start, listener, 4); + Assert.assertEquals(1, myTestInteraction.getNumPlaintextPrompt()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumPlaintextPrompt()); + Assert.assertEquals(4, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.PASSWORD); + + if (exception[0] != null) { + throw exception[0]; + } + SVNJNAUtil.setJNAEnabled(true); + } + + public void testPlaintextSSLPrompt() throws Exception { + SVNJNAUtil.setJNAEnabled(false); + + // yes, no + final TestListener listener = new TestListener(mySynchObject); + myAuthenticationManager.addListener(listener); + final SavedOnceListener savedOnceListener = new SavedOnceListener(); + myAuthenticationManager.addListener(savedOnceListener); + myTestInteraction.setSSLPlaintextAnswer(true); + + final SVNURL url = SVNURL.parseURIEncoded("https://some.host.com/repo"); + + final SVNException[] exception = new SVNException[1]; + final Boolean[] result = new Boolean[1]; + synchronousBackground(new Runnable() { + @Override + public void run() { + try { + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + listener.addStep(new Trinity(ProviderType.persistent, url, Type.save)); + + commonScheme(url, false, null); + long start = System.currentTimeMillis(); + waitListenerStep(start, listener, 3); + Assert.assertEquals(1, myTestInteraction.getNumSSLPlaintextPrompt()); + savedOnceListener.assertSaved(url, ISVNAuthenticationManager.SSL); + savedOnceListener.reset(); + myTestInteraction.reset(); + + myConfiguration.clearAuthenticationDirectory(); + myTestInteraction.setSSLPlaintextAnswer(false); + SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); + + listener.addStep(new Trinity(ProviderType.persistent, url, Type.request)); + listener.addStep(new Trinity(ProviderType.interactive, url, Type.request)); + commonScheme(url, false, null); + start = System.currentTimeMillis(); + waitListenerStep(start, listener, 5); + Assert.assertEquals(1, myTestInteraction.getNumSSLPlaintextPrompt()); + } + catch (SVNException e) { + exception[0] = e; + } + result[0] = true; + } + }); + + Assert.assertTrue(result[0]); + Assert.assertEquals(1, myTestInteraction.getNumSSLPlaintextPrompt()); + Assert.assertEquals(5, listener.getCnt()); + listener.assertForAwt(); + savedOnceListener.assertForAwt(); + savedOnceListener.assertNotSaved(url, ISVNAuthenticationManager.SSL); + + if (exception[0] != null) { + throw exception[0]; + } + SVNJNAUtil.setJNAEnabled(true); + } + + private void synchronousBackground(final Runnable runnable) throws InterruptedException { + final Semaphore semaphore = new Semaphore(); + semaphore.down(); + ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { + @Override + public void run() { + try { + runnable.run(); + } finally { + semaphore.up(); + } + } + }); + semaphore.waitFor(); + } + + private void commonScheme(final SVNURL url, final boolean username, final String realm) throws SVNException { + String kind = null; + + final String actualRealm = realm == null ? "realm" : realm; + final String protocol = url.getProtocol(); + if (username) { + kind = ISVNAuthenticationManager.USERNAME; + } else if ("svn+ssh".equals(protocol)) { + kind = ISVNAuthenticationManager.SSH; + } else if ("http".equals(protocol)) { + kind = ISVNAuthenticationManager.PASSWORD; + } else if ("https".equals(protocol)) { + kind = ISVNAuthenticationManager.SSL; + } else if ("file".equals(protocol)) { + kind = ISVNAuthenticationManager.USERNAME; + } + SVNAuthentication authentication = null; + try { + authentication = myAuthenticationManager.getFirstAuthentication(kind, actualRealm, url); + } finally { + myAuthenticationManager.acknowledgeAuthentication(authentication != null, kind, actualRealm, null, authentication); + } + } + + private static class SvnTestInteractiveAuthentication implements ISVNAuthenticationProvider { + private final SvnAuthenticationManager myManager; + private boolean mySaveData; + + public SvnTestInteractiveAuthentication(SvnAuthenticationManager manager) { + myManager = manager; + mySaveData = true; + } + + public void setSaveData(boolean saveData) { + mySaveData = saveData; + } + + @Override + public int acceptServerAuthentication(SVNURL url, String realm, Object certificate, boolean resultMayBeStored) { + return ISVNAuthenticationProvider.REJECTED; + } + + @Override + public SVNAuthentication requestClientAuthentication(String kind, + SVNURL url, + String realm, + SVNErrorMessage errorMessage, + SVNAuthentication previousAuth, + boolean authMayBeStored) { + authMayBeStored = authMayBeStored & mySaveData; + SVNAuthentication result = null; + if (ISVNAuthenticationManager.USERNAME.equals(kind)) { + result = new SVNUserNameAuthentication("username", authMayBeStored); + } else if (ISVNAuthenticationManager.PASSWORD.equals(kind)) { + result = new SVNPasswordAuthentication("username", "abc", authMayBeStored, url, false); + } else if (ISVNAuthenticationManager.SSH.equals(kind)) { + result = new SVNSSHAuthentication("username", "abc", -1, authMayBeStored, url, false); + } else if (ISVNAuthenticationManager.SSL.equals(kind)) { + result = new SVNSSLAuthentication(new File("aaa"), "abc", authMayBeStored, url, false); + } + if (! ISVNAuthenticationManager.USERNAME.equals(kind)) { + myManager.requested(ProviderType.interactive, url, realm, kind, result == null); + } + return result; + } + } + + private static class TestListener implements SvnAuthenticationListener { + private List> myExpectedSequence; + private int myCnt; + private final Object mySynchObject; + private boolean mySuccess; + + private TestListener(final Object synchObject) { + mySynchObject = synchObject; + myExpectedSequence = new ArrayList>(); + myCnt = 0; + mySuccess = true; + } + + public void addStep(Trinity step) { + myExpectedSequence.add(step); + } + + public int getCnt() { + return myCnt; + } + + @Override + public void actualSaveWillBeTried(ProviderType type, SVNURL url, String realm, String kind) { + if (! mySuccess) return; + + mySuccess = myExpectedSequence.get(myCnt).equals(new Trinity(type, url, Type.save)); + if (mySuccess) { + ++ myCnt; + } + synchronized (mySynchObject) { + mySynchObject.notifyAll(); + } + } + + @Override + public void requested(ProviderType type, SVNURL url, String realm, String kind, boolean canceled) { + if (! mySuccess) return; + + mySuccess = myExpectedSequence.get(myCnt).equals(new Trinity(type, url, Type.request)); + if (mySuccess) { + ++ myCnt; + } + synchronized (mySynchObject) { + mySynchObject.notifyAll(); + } + } + + public void assertForAwt() { + Assert.assertTrue("last cnt = " + myCnt, mySuccess); + } + } + + private static class SavedOnceListener implements SvnAuthenticationListener { + private final Set> myClientRequested; + private final Set> mySaved; + private String myCause; + + private SavedOnceListener() { + myClientRequested = new HashSet>(); + mySaved = new HashSet>(); + } + + public void reset() { + mySaved.clear(); + myClientRequested.clear(); + } + + @Override + public void actualSaveWillBeTried(ProviderType type, SVNURL url, String realm, String kind) { + final Pair pair = new Pair(url, kind); + if (mySaved.contains(pair)) { + myCause = "saved twice"; + } + mySaved.add(pair); + } + + public boolean isSaved(final SVNURL url, final String kind) { + return mySaved.contains(new Pair(url, kind)); + } + + @Override + public void requested(ProviderType type, SVNURL url, String realm, String kind, boolean canceled) { + if (ProviderType.interactive.equals(type)) { + final Pair pair = new Pair(url, kind); + if (myClientRequested.contains(pair)) { + myCause = "client requested twice"; + } + myClientRequested.add(pair); + } + } + + public void assertForAwt() { + Assert.assertTrue(myCause, myCause == null); + } + + public void assertSaved(final SVNURL url, final String kind) { + Assert.assertTrue("not saved", mySaved.contains(new Pair(url, kind))); + } + + public void assertNotSaved(final SVNURL url, final String kind) { + Assert.assertTrue("saved", ! mySaved.contains(new Pair(url, kind))); + } + } + + private static enum Type { + request, + save + } + + private static class TestInteraction implements SvnAuthenticationInteraction { + private int myNumAuthWarn; + private int myNumPasswordsWarn; + private int myNumSSLWarn; + private int myNumPlaintextPrompt; + private int myNumSSLPlaintextPrompt; + + private boolean myPlaintextAnswer; + private boolean mySSLPlaintextAnswer; + + public void setPlaintextAnswer(boolean plaintextAnswer) { + myPlaintextAnswer = plaintextAnswer; + } + + public void setSSLPlaintextAnswer(boolean SSLPlaintextAnswer) { + mySSLPlaintextAnswer = SSLPlaintextAnswer; + } + + public void assertNothing() { + Assert.assertEquals("myNumAuthWarn", myNumAuthWarn, 0); + Assert.assertEquals("myNumPasswordsWarn", myNumPasswordsWarn, 0); + Assert.assertEquals("myNumSSLWarn", myNumSSLWarn, 0); + Assert.assertEquals("myNumPlaintextPrompt", myNumPlaintextPrompt, 0); + Assert.assertEquals("myNumSSLPlaintextPrompt", myNumSSLPlaintextPrompt, 0); + } + + public void reset() { + myNumAuthWarn = 0; + myNumPasswordsWarn = 0; + myNumSSLWarn = 0; + myNumPlaintextPrompt = 0; + myNumSSLPlaintextPrompt = 0; + } + + @Override + public boolean promptForPlaintextPasswordSaving(SVNURL url, String realm) { + ++ myNumPlaintextPrompt; + return myPlaintextAnswer; + } + + @Override + public boolean promptInAwt() { + return false; + } + + @Override + public void warnOnAuthStorageDisabled(SVNURL url) { + ++ myNumAuthWarn; + } + + @Override + public void warnOnPasswordStorageDisabled(SVNURL url) { + ++ myNumPasswordsWarn; + } + + @Override + public void warnOnSSLPassphraseStorageDisabled(SVNURL url) { + ++ myNumSSLWarn; + } + + @Override + public boolean promptForSSLPlaintextPassphraseSaving(SVNURL url, String realm, File certificateFile) { + ++ myNumSSLPlaintextPrompt; + return mySSLPlaintextAnswer; + } + + public int getNumAuthWarn() { + return myNumAuthWarn; + } + + public int getNumPasswordsWarn() { + return myNumPasswordsWarn; + } + + public int getNumPlaintextPrompt() { + return myNumPlaintextPrompt; + } + + public int getNumSSLPlaintextPrompt() { + return myNumSSLPlaintextPrompt; + } + + public int getNumSSLWarn() { + return myNumSSLWarn; + } + } +}