1 /*
2  * NewRSConnectAuthPage.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.rsconnect.ui;
16 
17 import org.rstudio.core.client.StringUtil;
18 import org.rstudio.core.client.resources.ImageResource2x;
19 import org.rstudio.core.client.widget.OperationWithInput;
20 import org.rstudio.core.client.widget.ProgressIndicator;
21 import org.rstudio.core.client.widget.WizardPage;
22 import org.rstudio.studio.client.RStudioGinjector;
23 import org.rstudio.studio.client.application.Desktop;
24 import org.rstudio.studio.client.common.GlobalDisplay;
25 import org.rstudio.studio.client.common.GlobalDisplay.NewWindowOptions;
26 import org.rstudio.studio.client.common.Value;
27 import org.rstudio.studio.client.common.satellite.events.WindowClosedEvent;
28 import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountInput;
29 import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountResult;
30 import org.rstudio.studio.client.rsconnect.model.RSConnectAuthUser;
31 import org.rstudio.studio.client.rsconnect.model.RSConnectPreAuthToken;
32 import org.rstudio.studio.client.rsconnect.model.RSConnectServerInfo;
33 import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations;
34 import org.rstudio.studio.client.server.ServerError;
35 import org.rstudio.studio.client.server.ServerRequestCallback;
36 
37 import com.google.gwt.core.client.Scheduler;
38 import com.google.gwt.core.client.Scheduler.RepeatingCommand;
39 import com.google.gwt.event.logical.shared.ValueChangeEvent;
40 import com.google.gwt.event.logical.shared.ValueChangeHandler;
41 import com.google.gwt.user.client.Command;
42 import com.google.gwt.user.client.ui.Widget;
43 
44 public class NewRSConnectAuthPage
45    extends WizardPage<NewRSConnectAccountInput,
46                       NewRSConnectAccountResult>
47    implements WindowClosedEvent.Handler
48 {
NewRSConnectAuthPage()49    public NewRSConnectAuthPage()
50    {
51       super("", "", "Verifying Account",
52             new ImageResource2x(RSConnectResources.INSTANCE.localAccountIcon2x()),
53             new ImageResource2x(RSConnectResources.INSTANCE.localAccountIconLarge2x()));
54 
55       // listen for window close events (this page needs to know when the user
56       // closes the auth dialog
57       RStudioGinjector.INSTANCE.getEventBus().addHandler(
58             WindowClosedEvent.TYPE,
59             this);
60 
61       waitingForAuth_.addValueChangeHandler(new ValueChangeHandler<Boolean>()
62       {
63          @Override
64          public void onValueChange(ValueChangeEvent<Boolean> waiting)
65          {
66             if (setOkButtonVisible_ != null)
67                setOkButtonVisible_.execute(!waiting.getValue());
68          }
69       });
70    }
71 
72    @Override
focus()73    public void focus()
74    {
75    }
76 
77    @Override
setIntermediateResult(NewRSConnectAccountResult result)78    public void setIntermediateResult(NewRSConnectAccountResult result)
79    {
80       result_ = result;
81    }
82 
83    @Override
onActivate(final ProgressIndicator indicator)84    public void onActivate(final ProgressIndicator indicator)
85    {
86       if (waitingForAuth_.getValue() || result_ == null)
87          return;
88 
89       // save reference to parent wizard's progress indicator for retries
90       wizardIndicator_ = indicator;
91 
92       indicator.onProgress("Checking server connection...");
93       server_.validateServerUrl(result_.getServerUrl(),
94             new ServerRequestCallback<RSConnectServerInfo>()
95       {
96          @Override
97          public void onResponseReceived(RSConnectServerInfo info)
98          {
99             if (info.isValid())
100             {
101                result_.setServerInfo(info);
102                getPreAuthToken(indicator);
103             }
104             else
105             {
106                contents_.showError("Server Validation Failed",
107                      "The URL '" + result_.getServerUrl() +
108                      "' does not appear to belong to a valid server. Please " +
109                      "double check the URL, and contact your administrator " +
110                      "if the problem persists.\n\n" +
111                      info.getMessage());
112                indicator.clearProgress();
113             }
114          }
115 
116          @Override
117          public void onError(ServerError error)
118          {
119             contents_.showError("Error Connecting Account",
120                   "The server couldn't be validated. " +
121                    error.getMessage());
122             indicator.clearProgress();
123          }
124       });
125    }
126 
127    @Override
onWindowClosed(WindowClosedEvent event)128    public void onWindowClosed(WindowClosedEvent event)
129    {
130    }
131 
132    @Override
onWizardClosing()133    public void onWizardClosing()
134    {
135       // this will cause us to stop polling for auth (if we haven't already)
136       waitingForAuth_.setValue(false, true);
137    }
138 
setOkButtonVisible(OperationWithInput<Boolean> okButtonVisible)139    public void setOkButtonVisible(OperationWithInput<Boolean> okButtonVisible)
140    {
141       setOkButtonVisible_ = okButtonVisible;
142    }
143 
144    @Override
createWidget()145    protected Widget createWidget()
146    {
147       contents_ = new RSConnectAuthWait();
148       contents_.setOnTryAgain(new Command()
149       {
150          @Override
151          public void execute()
152          {
153             onActivate(wizardIndicator_);
154          }
155       });
156       return contents_;
157    }
158 
159    @Override
initialize(NewRSConnectAccountInput initData)160    protected void initialize(NewRSConnectAccountInput initData)
161    {
162       server_ = initData.getServer();
163       display_ = initData.getDisplay();
164    }
165 
166    @Override
collectInput()167    protected NewRSConnectAccountResult collectInput()
168    {
169       return result_;
170    }
171 
172    @Override
validate(NewRSConnectAccountResult input)173    protected boolean validate(NewRSConnectAccountResult input)
174    {
175       return input != null && input.getAuthUser() != null;
176    }
177 
pollForAuthCompleted()178    private void pollForAuthCompleted()
179    {
180       Scheduler.get().scheduleFixedDelay(new RepeatingCommand()
181       {
182          @Override
183          public boolean execute()
184          {
185             // don't keep polling once auth is complete or window is closed
186             if (!waitingForAuth_.getValue())
187                return false;
188 
189             // avoid re-entrancy--if we're already running a check but it hasn't
190             // returned for some reason, just wait for it to finish
191             if (runningAuthCompleteCheck_)
192                return true;
193 
194             runningAuthCompleteCheck_ = true;
195             server_.getUserFromToken(result_.getServerInfo().getUrl(),
196                   result_.getPreAuthToken(),
197                   new ServerRequestCallback<RSConnectAuthUser>()
198                   {
199                      @Override
200                      public void onResponseReceived(RSConnectAuthUser user)
201                      {
202                         runningAuthCompleteCheck_ = false;
203 
204                         // expected if user hasn't finished authenticating yet,
205                         // just wait and try again
206                         if (!user.isValidUser())
207                            return;
208 
209                         // user is valid--cache account info and close the
210                         // window
211                         result_.setAuthUser(user);
212                         waitingForAuth_.setValue(false, true);
213 
214                         onUserAuthVerified();
215                      }
216 
217                      @Override
218                      public void onError(ServerError error)
219                      {
220                         // ignore this error
221                         runningAuthCompleteCheck_ = false;
222                      }
223                   });
224             return true;
225          }
226       }, 1000);
227    }
228 
229    @SuppressWarnings("unused")
onAuthCompleted()230    private void onAuthCompleted()
231    {
232       server_.getUserFromToken(result_.getServerInfo().getUrl(),
233             result_.getPreAuthToken(),
234             new ServerRequestCallback<RSConnectAuthUser>()
235       {
236          @Override
237          public void onResponseReceived(RSConnectAuthUser user)
238          {
239             if (!user.isValidUser())
240             {
241                contents_.showError("Account Not Connected",
242                      "Authentication failed. If you did not cancel " +
243                      "authentication, try again, or contact your server " +
244                      "administrator for assistance.");
245             }
246             else
247             {
248                result_.setAuthUser(user);
249                onUserAuthVerified();
250             }
251          }
252 
253          @Override
254          public void onError(ServerError error)
255          {
256             contents_.showError("Account Validation Failed",
257                   "RStudio failed to determine whether the account was " +
258                   "valid. Try again; if the error persists, contact your " +
259                   "server administrator.\n\n" +
260                   result_.getServerInfo().getInfoString() + "\n" +
261                   error.getMessage());
262          }
263       });
264    }
265 
onUserAuthVerified()266    private void onUserAuthVerified()
267    {
268       // set the account nickname if we didn't already have one
269       if (result_.getAccountNickname().length() == 0)
270       {
271          if (result_.getAuthUser().getUsername().length() > 0)
272          {
273             // if we have a username already, just use it
274             result_.setAccountNickname(
275                   result_.getAuthUser().getUsername());
276          }
277          else
278          {
279             // if we don't have any username, guess one based on user's given name
280             // on the server
281             result_.setAccountNickname(
282                   result_.getAuthUser().getFirstName().substring(0, 1) +
283                   result_.getAuthUser().getLastName().toLowerCase());
284          }
285       }
286 
287       contents_.showSuccess(result_.getServerName(),
288             result_.getAccountNickname());
289    }
290 
getPreAuthToken(ProgressIndicator indicator)291    private void getPreAuthToken(ProgressIndicator indicator)
292    {
293       getPreAuthToken(result_, result_.getServerInfo(), indicator,
294             new OperationWithInput<NewRSConnectAccountResult>()
295             {
296                @Override
297                public void execute(NewRSConnectAccountResult input)
298                {
299                   // do nothing if no result returned
300                   if (input == null)
301                      return;
302 
303                   // save intermediate result
304                   result_ = input;
305 
306                   contents_.setClaimLink(result_.getServerInfo().getName(),
307                         result_.getPreAuthToken().getClaimUrl());
308 
309                   // begin waiting for user to complete authentication
310                   waitingForAuth_.setValue(true, true);
311                   contents_.showWaiting();
312 
313                   // prepare a new window with the auth URL loaded
314                   if (Desktop.hasDesktopFrame())
315                   {
316                      Desktop.getFrame().browseUrl(StringUtil.notNull(result_.getPreAuthToken().getClaimUrl()));
317                   }
318                   else
319                   {
320                      NewWindowOptions options = new NewWindowOptions();
321                      options.setAllowExternalNavigation(true);
322                      options.setShowDesktopToolbar(false);
323                      display_.openWebMinimalWindow(
324                            result_.getPreAuthToken().getClaimUrl(),
325                            false,
326                            700, 800, options);
327                   }
328 
329                   // close the window automatically when authentication finishes
330                   pollForAuthCompleted();
331                }
332             });
333    }
334 
getPreAuthToken( final NewRSConnectAccountResult result, final RSConnectServerInfo serverInfo, final ProgressIndicator indicator, final OperationWithInput<NewRSConnectAccountResult> onResult)335    private void getPreAuthToken(
336          final NewRSConnectAccountResult result,
337          final RSConnectServerInfo serverInfo,
338          final ProgressIndicator indicator,
339          final OperationWithInput<NewRSConnectAccountResult> onResult)
340    {
341       indicator.onProgress("Setting up an account...");
342       server_.getPreAuthToken(serverInfo.getName(),
343             new ServerRequestCallback<RSConnectPreAuthToken>()
344       {
345          @Override
346          public void onResponseReceived(final RSConnectPreAuthToken token)
347          {
348             NewRSConnectAccountResult newResult = result;
349             newResult.setPreAuthToken(token);
350             newResult.setServerInfo(serverInfo);
351             onResult.execute(newResult);
352             indicator.clearProgress();
353          }
354 
355          @Override
356          public void onError(ServerError error)
357          {
358             display_.showErrorMessage("Error Connecting Account",
359                   "The server appears to be valid, but rejected the " +
360                   "request to authorize an account.\n\n"+
361                   serverInfo.getInfoString() + "\n" +
362                   error.getMessage());
363             indicator.clearProgress();
364             onResult.execute(null);
365          }
366       });
367    }
368 
369    private OperationWithInput<Boolean> setOkButtonVisible_;
370 
371    private NewRSConnectAccountResult result_;
372    private RSConnectServerOperations server_;
373    private GlobalDisplay display_;
374    private RSConnectAuthWait contents_;
375    private Value<Boolean> waitingForAuth_ = new Value<>(false);
376    private boolean runningAuthCompleteCheck_ = false;
377    private ProgressIndicator wizardIndicator_;
378 }
379