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