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">×</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>
