Repository: incubator-zeppelin
Updated Branches:
  refs/heads/master 729d49a0f -> 1852ab6f7


Replace standard basic-auth dialog with BootstrapDialog for user authentication

### What is this PR for?
Instead of showing standard basic-auth dialog, replace the same with 
BootstrapDialog for user authentication.

### What type of PR is it?
Improvement

### Todos
* [x] - Add API for user authentication.
* [x] - UI to take username/password.

### What is the Jira issue?
* [ZEPPELIN-769](https://issues.apache.org/jira/browse/ZEPPELIN-769)

### How should this be tested?
Edit `shiro.ini` replace line 39,40 with

    #/** = anon
    /** = authc

### Screenshots (if appropriate)

#### Before:

<img width="720" alt="screen shot 2016-03-28 at 3 21 44 pm" 
src="https://cloud.githubusercontent.com/assets/674497/14075699/de7403b6-f4f8-11e5-99ad-4a45daae76cd.png";>

#### After:

<img width="1438" alt="screen shot 2016-03-28 at 3 18 18 pm" 
src="https://cloud.githubusercontent.com/assets/674497/14075666/9d8e33c6-f4f8-11e5-8ae0-9d1d25022d20.png";>
<img width="1438" alt="screen shot 2016-03-28 at 3 18 37 pm" 
src="https://cloud.githubusercontent.com/assets/674497/14075667/9d90f75a-f4f8-11e5-96ba-ede36da91336.png";>
<img width="1438" alt="screen shot 2016-03-28 at 3 19 33 pm" 
src="https://cloud.githubusercontent.com/assets/674497/14075668/9d91d5a8-f4f8-11e5-9df8-11f63b3629f9.png";>

Author: Prabhjyot Singh <[email protected]>

Closes #801 from prabhjyotsingh/loginPage and squashes the following commits:

61f9205 [Prabhjyot Singh] restrict access to notebook without login
8d9b07c [Prabhjyot Singh] use modal dialog instead of basic auth dialog


Project: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/commit/1852ab6f
Tree: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/tree/1852ab6f
Diff: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/diff/1852ab6f

Branch: refs/heads/master
Commit: 1852ab6f7f6029535a4fcb8305352f9d39f43a7e
Parents: 729d49a
Author: Prabhjyot Singh <[email protected]>
Authored: Tue Mar 29 09:01:57 2016 +0530
Committer: Lee moon soo <[email protected]>
Committed: Thu Mar 31 08:05:21 2016 -0700

----------------------------------------------------------------------
 conf/shiro.ini                                  |   5 +-
 .../org/apache/zeppelin/rest/LoginRestApi.java  | 112 +++++++++++++++++++
 .../apache/zeppelin/server/ZeppelinServer.java  |   3 +
 zeppelin-web/src/app/home/home.controller.js    |   6 +-
 .../src/components/login/login.controller.js    |  42 +++++++
 zeppelin-web/src/components/login/login.html    |  51 +++++++++
 .../src/components/navbar/navbar.controller.js  |  28 ++++-
 zeppelin-web/src/components/navbar/navbar.html  |   3 +
 .../websocketEvents/websocketEvents.factory.js  |  12 +-
 zeppelin-web/src/index.html                     |   4 +
 10 files changed, 254 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/conf/shiro.ini
----------------------------------------------------------------------
diff --git a/conf/shiro.ini b/conf/shiro.ini
index 8d803d0..3b161f0 100644
--- a/conf/shiro.ini
+++ b/conf/shiro.ini
@@ -29,11 +29,12 @@ user3 = password4, role2
 #ldapRealm.userDnTemplate = cn={0},cn=engg,ou=testdomain,dc=testdomain,dc=com
 #ldapRealm.contextFactory.url = ldap://ldaphost:389
 #ldapRealm.contextFactory.authenticationMechanism = SIMPLE
+shiro.loginUrl = /api/login
 
 [urls]
 # anon means the access is anonymous.
 # authcBasic means Basic Auth Security
 # To enfore security, comment the line below and uncomment the next one
+/api/version = anon
 /** = anon
-#/** = authcBasic
-
+#/** = authc

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java
new file mode 100644
index 0000000..2cf707c
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.zeppelin.rest;
+
+import org.apache.shiro.authc.*;
+import org.apache.shiro.session.Session;
+import org.apache.shiro.subject.Subject;
+import org.apache.zeppelin.server.JsonResponse;
+import org.apache.zeppelin.ticket.TicketContainer;
+import org.apache.zeppelin.utils.SecurityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.FormParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Created for org.apache.zeppelin.rest.message on 17/03/16.
+ */
+
+@Path("/login")
+@Produces("application/json")
+public class LoginRestApi {
+  private static final Logger LOG = 
LoggerFactory.getLogger(LoginRestApi.class);
+
+  /**
+   * Required by Swagger.
+   */
+  public LoginRestApi() {
+    super();
+  }
+
+
+  /**
+   * Post Login
+   * Returns userName & password
+   * for anonymous access, username is always anonymous.
+   * After getting this ticket, access through websockets become safe
+   *
+   * @return 200 response
+   */
+  @POST
+  public Response postLogin(@FormParam("userName") String userName,
+                            @FormParam("password") String password) {
+    JsonResponse response = null;
+    // ticket set to anonymous for anonymous user. Simplify testing.
+    Subject currentUser = org.apache.shiro.SecurityUtils.getSubject();
+    if (!currentUser.isAuthenticated()) {
+      try {
+        UsernamePasswordToken token = new UsernamePasswordToken(userName, 
password);
+        //      token.setRememberMe(true);
+        currentUser.login(token);
+        HashSet<String> roles = SecurityUtils.getRoles();
+        String principal = SecurityUtils.getPrincipal();
+        String ticket;
+        if ("anonymous".equals(principal))
+          ticket = "anonymous";
+        else
+          ticket = TicketContainer.instance.getTicket(principal);
+
+        Map<String, String> data = new HashMap<>();
+        data.put("principal", principal);
+        data.put("roles", roles.toString());
+        data.put("ticket", ticket);
+
+        response = new JsonResponse(Response.Status.OK, "", data);
+        //if no exception, that's it, we're done!
+      } catch (UnknownAccountException uae) {
+        //username wasn't in the system, show them an error message?
+        LOG.error("Exception in login: ", uae);
+      } catch (IncorrectCredentialsException ice) {
+        //password didn't match, try again?
+        LOG.error("Exception in login: ", ice);
+      } catch (LockedAccountException lae) {
+        //account for that username is locked - can't login.  Show them a 
message?
+        LOG.error("Exception in login: ", lae);
+      } catch (AuthenticationException ae) {
+        //unexpected condition - error?
+        LOG.error("Exception in login: ", ae);
+      }
+    }
+
+    if (response == null) {
+      response = new JsonResponse(Response.Status.FORBIDDEN, "", "");
+    }
+
+    LOG.warn(response.toString());
+    return response.build();
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
index bfd3f90..9fe8dab 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
@@ -290,6 +290,9 @@ public class ZeppelinServer extends Application {
     SecurityRestApi securityApi = new SecurityRestApi();
     singletons.add(securityApi);
 
+    LoginRestApi loginRestApi = new LoginRestApi();
+    singletons.add(loginRestApi);
+
     ConfigurationsRestApi settingsApi = new ConfigurationsRestApi(notebook);
     singletons.add(settingsApi);
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-web/src/app/home/home.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/home/home.controller.js 
b/zeppelin-web/src/app/home/home.controller.js
index e57a983..14f1587 100644
--- a/zeppelin-web/src/app/home/home.controller.js
+++ b/zeppelin-web/src/app/home/home.controller.js
@@ -20,7 +20,11 @@ angular.module('zeppelinWebApp').controller('HomeCtrl', 
function($scope, noteboo
   vm.arrayOrderingSrv = arrayOrderingSrv;
 
   vm.notebookHome = false;
-  vm.staticHome = false;
+  if ($rootScope.ticket !== undefined) {
+    vm.staticHome = false;
+  } else {
+    vm.staticHome = true;
+  }
 
   $scope.isReloading = false;
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-web/src/components/login/login.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/login/login.controller.js 
b/zeppelin-web/src/components/login/login.controller.js
new file mode 100644
index 0000000..3a4f535
--- /dev/null
+++ b/zeppelin-web/src/components/login/login.controller.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+angular.module('zeppelinWebApp').controller('LoginCtrl',
+  function($scope, $rootScope, $http, $httpParamSerializer, baseUrlSrv) {
+    $scope.loginParams = {};
+    $scope.login = function() {
+
+      $http({
+        method: 'POST',
+        url: baseUrlSrv.getRestApiBase() + '/login',
+        headers: {
+          'Content-Type': 'application/x-www-form-urlencoded'
+        },
+        data: $httpParamSerializer({
+          'userName': $scope.loginParams.userName,
+          'password': $scope.loginParams.password
+        })
+      }).then(function successCallback(response) {
+        $rootScope.ticket = response.data.body;
+        angular.element('#loginModal').modal('toggle');
+        $rootScope.$broadcast('loginSuccess', true);
+      }, function errorCallback(errorResponse) {
+        $scope.loginParams.errorText = 'The username and password that you 
entered don\'t match.';
+      });
+
+    };
+  }
+);

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-web/src/components/login/login.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/login/login.html 
b/zeppelin-web/src/components/login/login.html
new file mode 100644
index 0000000..5200040
--- /dev/null
+++ b/zeppelin-web/src/components/login/login.html
@@ -0,0 +1,51 @@
+<!--
+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.
+-->
+
+  <div id="loginModal" class="modal fade" role="dialog"
+       tabindex='-1'>
+    <div class="modal-dialog">
+
+      <!-- Modal content-->
+      <div class="modal-content" id="NoteImportCtrl" ng-init="NoteImportInit">
+        <div class="modal-header">
+          <button type="button" class="close" 
data-dismiss="modal">&times;</button>
+          <h4 class="modal-title">Login</h4>
+        </div>
+        <div class="modal-body">
+          <div class="form-group" ng-show="loginParams.errorText">
+            <div class="alert alert-danger">{{loginParams.errorText}}</div>
+          </div>
+          <div class="form-group">
+            <label for="userName">User Name</label>
+            <input placeholder="User Name" type="text" class="form-control" 
id="userName"
+                   ng-keypress="loginParams.errorText = ''"
+                   ng-model="loginParams.userName">
+          </div>
+          <div class="form-group">
+            <label for="password">Password</label>
+            <input placeholder="Password" type="password" class="form-control" 
id="password"
+                   ng-enter="login()"
+                   ng-keypress="loginParams.errorText = ''"
+                   ng-model="loginParams.password">
+          </div>
+
+        </div>
+        <div class="modal-footer">
+          <div>
+            <button type="button" class="btn btn-default btn-primary" 
ng-click="login()">Login</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-web/src/components/navbar/navbar.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js 
b/zeppelin-web/src/components/navbar/navbar.controller.js
index f04522f..02db331 100644
--- a/zeppelin-web/src/components/navbar/navbar.controller.js
+++ b/zeppelin-web/src/components/navbar/navbar.controller.js
@@ -18,13 +18,22 @@ angular.module('zeppelinWebApp').controller('NavCtrl', 
function($scope, $rootSco
     $location, notebookListDataFactory, websocketMsgSrv, arrayOrderingSrv) {
   /** Current list of notes (ids) */
 
+  $scope.showLoginWindow = function() {
+    setTimeout(function() {
+      angular.element('#userName').focus();
+    }, 500);
+  };
+
   var vm = this;
   vm.notes = notebookListDataFactory;
   vm.connected = websocketMsgSrv.isConnected();
   vm.websocketMsgSrv = websocketMsgSrv;
   vm.arrayOrderingSrv = arrayOrderingSrv;
-  $rootScope.fullUsername = $rootScope.ticket.principal;
-  $rootScope.truncatedUsername = $rootScope.ticket.principal;
+  if ($rootScope.ticket) {
+    $rootScope.fullUsername = $rootScope.ticket.principal;
+    $rootScope.truncatedUsername = $rootScope.ticket.principal;
+  }
+
   var MAX_USERNAME_LENGTH=16;
 
   angular.element('#notebook-list').perfectScrollbar({suppressScrollX: true});
@@ -47,14 +56,21 @@ angular.module('zeppelinWebApp').controller('NavCtrl', 
function($scope, $rootSco
   });
 
   $scope.checkUsername = function () {
-    if($rootScope.ticket.principal.length <= MAX_USERNAME_LENGTH) {
-       $rootScope.truncatedUsername=$rootScope.ticket.principal;
+    if ($rootScope.ticket) {
+      if ($rootScope.ticket.principal.length <= MAX_USERNAME_LENGTH) {
+        $rootScope.truncatedUsername = $rootScope.ticket.principal;
       }
-    else {
-           
$rootScope.truncatedUsername=$rootScope.ticket.principal.substr(0,MAX_USERNAME_LENGTH)+'..';
+      else {
+        $rootScope.truncatedUsername = $rootScope.ticket.principal.substr(0, 
MAX_USERNAME_LENGTH) + '..';
       }
+    }
   };
 
+  $scope.$on('loginSuccess', function(event, param) {
+    $scope.checkUsername();
+    loadNotes();
+  });
+
   $scope.search = function() {
     $location.url(/search/ + $scope.searchTerm);
   };

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-web/src/components/navbar/navbar.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/navbar/navbar.html 
b/zeppelin-web/src/components/navbar/navbar.html
index f6d386f..bdc5790 100644
--- a/zeppelin-web/src/components/navbar/navbar.html
+++ b/zeppelin-web/src/components/navbar/navbar.html
@@ -80,6 +80,9 @@ limitations under the License.
         <span ng-show="navbar.connected" ng-if="ticket.principal != 
'anonymous' " tooltip-placement="bottom" 
tooltip="{{fullUsername}}">{{truncatedUsername}}</span>
         <span ng-show="!navbar.connected">Disconnected</span>
         </li>
+        <li ng-if="!ticket">
+            <button class="btn btn-default" data-toggle="modal" 
data-target="#loginModal" ng-click="showLoginWindow()" style="margin-left: 
10px">Login</button>
+        </li>
       </ul>
     </div>
   </div>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
----------------------------------------------------------------------
diff --git 
a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js 
b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
index f40c47f..7ea70f8 100644
--- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
+++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
@@ -28,9 +28,15 @@ angular.module('zeppelinWebApp').factory('websocketEvents', 
function($rootScope,
   });
 
   websocketCalls.sendNewEvent = function(data) {
-    data.principal = $rootScope.ticket.principal;
-    data.ticket = $rootScope.ticket.ticket;
-    data.roles = $rootScope.ticket.roles;
+    if ($rootScope.ticket !== undefined) {
+      data.principal = $rootScope.ticket.principal;
+      data.ticket = $rootScope.ticket.ticket;
+      data.roles = $rootScope.ticket.roles;
+    } else {
+      data.principal = '';
+      data.ticket = '';
+      data.roles = '';
+    }
     console.log('Send >> %o, %o, %o, %o, %o', data.op, data.principal, 
data.ticket, data.roles, data);
     websocketCalls.ws.send(JSON.stringify(data));
   };

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/1852ab6f/zeppelin-web/src/index.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html
index 1f1166b..bd0dbae 100644
--- a/zeppelin-web/src/index.html
+++ b/zeppelin-web/src/index.html
@@ -79,6 +79,9 @@ limitations under the License.
     <div ng-controller="NoteImportCtrl as noteimportctrl">
       <div id="note-import-container" ng-include 
src="'components/noteName-import/note-import-dialog.html'"></div>
     </div>
+    <div ng-controller="LoginCtrl as noteimportctrl">
+      <div id="login-container" ng-include 
src="'components/login/login.html'"></div>
+    </div>
     <!-- build:js(.) scripts/oldieshim.js -->
     <!--[if lt IE 9]>
     <script src="bower_components/es5-shim/es5-shim.js"></script>
@@ -155,6 +158,7 @@ limitations under the License.
     <script src="components/browser-detect/browserDetect.service.js"></script>
     <script src="components/saveAs/saveAs.service.js"></script>
     <script src="components/searchService/search.service.js"></script>
+    <script src="components/login/login.controller.js"></script>
     <!-- endbuild -->
   </body>
 </html>

Reply via email to