summaryrefslogtreecommitdiff
path: root/plugins/git4idea/src/git4idea/commands/GitHttpGuiAuthenticator.java
blob: e0312e5471fc6c47bbd3508e2f33804d9428fed5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/*
 * Copyright 2000-2014 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 git4idea.commands;

import com.intellij.ide.passwordSafe.PasswordSafe;
import com.intellij.ide.passwordSafe.PasswordSafeException;
import com.intellij.ide.passwordSafe.impl.PasswordSafeImpl;
import com.intellij.ide.passwordSafe.ui.PasswordSafePromptDialog;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.AuthData;
import com.intellij.util.UriUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.URLUtil;
import com.intellij.vcsUtil.AuthDialog;
import git4idea.remote.GitHttpAuthDataProvider;
import git4idea.remote.GitRememberedInputs;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.List;

/**
 * <p>Handles "ask username" and "ask password" requests from Git:
 *    shows authentication dialog in the GUI, waits for user input and returns the credentials supplied by the user.</p>
 * <p>If user cancels the dialog, empty string is returned.</p>
 * <p>If no username is specified in the URL, Git queries for the username and for the password consecutively.
 *    In this case to avoid showing dialogs twice, the component asks for both credentials at once,
 *    and remembers the password to provide it to the Git process during the next request without requiring user interaction.</p>
 * <p>New instance of the GitAskPassGuiHandler should be created for each session, i. e. for each remote operation call.</p>
 *
 * @author Kirill Likhodedov
 */
class GitHttpGuiAuthenticator implements GitHttpAuthenticator {

  private static final Logger LOG = Logger.getInstance(GitHttpGuiAuthenticator.class);
  private static final Class<GitHttpAuthenticator> PASS_REQUESTER = GitHttpAuthenticator.class;

  @NotNull  private final Project myProject;
  @Nullable private final ModalityState myModalityState;
  @NotNull  private final String myTitle;
  @NotNull private final String myUrlFromCommand;

  @Nullable private String myPassword;
  @Nullable private String myPasswordKey;
  @Nullable private String myUrl;
  @Nullable private String myLogin;
  private boolean mySaveOnDisk;
  @Nullable private GitHttpAuthDataProvider myDataProvider;
  private boolean myWasCancelled;

  GitHttpGuiAuthenticator(@NotNull Project project, @Nullable ModalityState modalityState, @NotNull GitCommand command,
                          @NotNull String url) {
    myProject = project;
    myModalityState = modalityState;
    myTitle = "Git " + StringUtil.capitalize(command.name());
    myUrlFromCommand = url;
  }

  @Override
  @NotNull
  public String askPassword(@NotNull String url) {
    if (myPassword != null) {  // already asked in askUsername
      return myPassword;
    }
    if (myWasCancelled) { // already pressed cancel in askUsername
      return "";
    }
    url = adjustUrl(url);
    Pair<GitHttpAuthDataProvider, AuthData> authData = findBestAuthData(url, myModalityState);
    if (authData != null && authData.second.getPassword() != null) {
      String password = authData.second.getPassword();
      myDataProvider = authData.first;
      myPassword = password;
      return password;
    }

    String prompt = "Enter the password for " + url;
    myPasswordKey = url;
    String password = PasswordSafePromptDialog.askPassword(myProject, myModalityState, myTitle, prompt, PASS_REQUESTER, url, false, null);
    if (password == null) {
      myWasCancelled = true;
      return "";
    }
    // Password is stored in the safe in PasswordSafePromptDialog.askPassword,
    // but it is not the right behavior (incorrect password is stored too because of that) and should be fixed separately.
    // We store it here manually, to let it work after that behavior is fixed.
    myPassword = password;
    myDataProvider = new GitDefaultHttpAuthDataProvider(); // workaround: askPassword remembers the password even it is not correct
    return password;
  }

  @Override
  @NotNull
  public String askUsername(@NotNull String url) {
    url = adjustUrl(url);
    Pair<GitHttpAuthDataProvider, AuthData> authData = findBestAuthData(url, myModalityState);
    String login = null;
    String password = null;
    if (authData != null) {
      login = authData.second.getLogin();
      password = authData.second.getPassword();
      myDataProvider = authData.first;
    }
    if (login != null && password != null) {
      myPassword = password;
      return login;
    }

    AuthDialog dialog = showAuthDialog(url, login);
    if (dialog == null || !dialog.isOK()) {
      myWasCancelled = true;
      return "";
    }

    // remember values to store in the database afterwards, if authentication succeeds
    myPassword = dialog.getPassword();
    myLogin = dialog.getUsername();
    myUrl = url;
    mySaveOnDisk = dialog.isRememberPassword();
    myPasswordKey = makeKey(myUrl, myLogin);

    return myLogin;
  }

  @Nullable
  private AuthDialog showAuthDialog(final String url, final String login) {
    final Ref<AuthDialog> dialog = Ref.create();
    ApplicationManager.getApplication().invokeAndWait(new Runnable() {
      @Override
      public void run() {
        dialog.set(new AuthDialog(myProject, myTitle, "Enter credentials for " + url, login, null, true));
        dialog.get().show();
      }
    }, myModalityState == null ? ModalityState.defaultModalityState() : myModalityState);
    return dialog.get();
  }

  @Override
  public void saveAuthData() {
    // save login and url
    if (myUrl != null && myLogin != null) {
      GitRememberedInputs.getInstance().addUrl(myUrl, myLogin);
    }

    // save password
    if (myPasswordKey != null && myPassword != null) {
      PasswordSafeImpl passwordSafe = (PasswordSafeImpl)PasswordSafe.getInstance();
      try {
        passwordSafe.getMemoryProvider().storePassword(myProject, PASS_REQUESTER, myPasswordKey, myPassword);
        if (mySaveOnDisk) {
          passwordSafe.getMasterKeyProvider().storePassword(myProject, PASS_REQUESTER, myPasswordKey, myPassword);
        }
      }
      catch (PasswordSafeException e) {
        LOG.error("Couldn't remember password for " + myPasswordKey, e);
      }
    }
  }

  @Override
  public void forgetPassword() {
    if (myDataProvider != null) {
      myDataProvider.forgetPassword(adjustUrl(myUrl));
    }
  }

  @Override
  public boolean wasCancelled() {
    return myWasCancelled;
  }

  @NotNull
  private String adjustUrl(@Nullable String url) {
    if (StringUtil.isEmptyOrSpaces(url)) {
      // if Git doesn't specify the URL in the username/password query, we use the url from the Git command
      // We only take the host, to avoid entering the same password for different repositories on the same host.
      return adjustHttpUrl(getHost(myUrlFromCommand));
    }
    return adjustHttpUrl(url);
  }

  @NotNull
  private static String getHost(@NotNull String url) {
    Couple<String> split = UriUtil.splitScheme(url);
    String scheme = split.getFirst();
    String urlItself = split.getSecond();
    int pathStart = urlItself.indexOf("/");
    return scheme + URLUtil.SCHEME_SEPARATOR + urlItself.substring(0, pathStart);
  }

  /**
   * If the url scheme is HTTPS, store it as HTTP in the database, not to make user enter and remember same credentials twice.
   */
  @NotNull
  private static String adjustHttpUrl(@NotNull String url) {
    String prefix = "https";
    if (url.startsWith(prefix)) {
      return "http" + url.substring(prefix.length());
    }
    return url;
  }

  // return the first that knows username + password; otherwise return the first that knows just the username
  @Nullable
  private Pair<GitHttpAuthDataProvider, AuthData> findBestAuthData(@NotNull String url, @Nullable ModalityState modalityState) {
    Pair<GitHttpAuthDataProvider, AuthData> candidate = null;
    for (GitHttpAuthDataProvider provider : getProviders()) {
      AuthData data = provider.getAuthData(url, modalityState);
      if (data != null) {
        Pair<GitHttpAuthDataProvider, AuthData> pair = Pair.create(provider, data);
        if (data.getPassword() != null) {
          return pair;
        }
        if (candidate == null) {
          candidate = pair;
        }
      }
    }
    return candidate;
  }
  
  @NotNull
  private List<GitHttpAuthDataProvider> getProviders() {
    List<GitHttpAuthDataProvider> providers = ContainerUtil.newArrayList();
    providers.add(new GitDefaultHttpAuthDataProvider());
    providers.addAll(Arrays.asList(GitHttpAuthDataProvider.EP_NAME.getExtensions()));
    return providers;
  }

  /**
   * Makes the password database key for the URL: inserts the login after the scheme: http://login@url.
   */
  @NotNull
  private static String makeKey(@NotNull String url, @Nullable String login) {
    if (login == null) {
      return url;
    }
    Couple<String> pair = UriUtil.splitScheme(url);
    String scheme = pair.getFirst();
    if (StringUtil.isEmpty(scheme)) {
      return scheme + URLUtil.SCHEME_SEPARATOR + login + "@" + pair.getSecond();
    }
    return login + "@" + url;
  }

  public class GitDefaultHttpAuthDataProvider implements GitHttpAuthDataProvider {

    @Nullable
    @Override
    public AuthData getAuthData(@NotNull String url, @Nullable ModalityState modalityState) {
      String userName = getUsername(url);
      String key = makeKey(url, userName);
      final PasswordSafe passwordSafe = PasswordSafe.getInstance();
      try {
        String password = passwordSafe.getPassword(myProject, PASS_REQUESTER, key, modalityState);
        return new AuthData(StringUtil.notNullize(userName), password);
      }
      catch (PasswordSafeException e) {
        LOG.info("Couldn't get the password for key [" + key + "]", e);
        return null;
      }
    }

    @Nullable
    private String getUsername(@NotNull String url) {
      return GitRememberedInputs.getInstance().getUserNameForUrl(url);
    }

    @Override
    public void forgetPassword(@NotNull String url) {
      String key = myPasswordKey != null ? myPasswordKey : makeKey(url, getUsername(url));
      try {
        PasswordSafe.getInstance().removePassword(myProject, PASS_REQUESTER, key);
      }
      catch (PasswordSafeException e) {
        LOG.info("Couldn't forget the password for " + myPasswordKey);
      }
    }
  }

}