Repository: zeppelin Updated Branches: refs/heads/master ed4680033 -> 6caf587e1
[ZEPPELIN-3090] Support KnoxSSO Authentication ### What is this PR for? Zeppelin to support KnoxSSO Authentication method. ### What type of PR is it? [Bug Fix | Improvement] ### What is the Jira issue? * [https://issues.apache.org/jira/browse/ZEPPELIN-3090](https://issues.apache.org/jira/browse/ZEPPELIN-3090) ### How should this be tested? This will require new shiro conf ``` knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm knoxJwtRealm.providerUrl = https://domain.example.com/ knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout knoxJwtRealm.redirectParam = originalUrl knoxJwtRealm.cookieName = hadoop-jwt knoxJwtRealm.publicKeyPath = /etc/zeppelin/conf/knox-sso.pem knoxJwtRealm.groupPrincipalMapping = group.principal.mapping knoxJwtRealm.principalMapping = principal.mapping ``` Refer screenshot section for demo ### Screenshots (if appropriate)  ### Questions: * Does the licenses files need update? yes * Is there breaking changes for older versions? no * Does this needs documentation? yes Author: Prabhjyot Singh <prabhjyotsi...@gmail.com> Author: prabhjyotsingh <prabhjyotsi...@gmail.com> Closes #2694 from prabhjyotsingh/KNOX_SSO and squashes the following commits: 05ed844ac [Prabhjyot Singh] Add unit test case 187b5678f [Prabhjyot Singh] fix failing " PersonalizeActionsIT.testGraphAction:263 The output of graph mode is not changed" test. 51f13521c [Prabhjyot Singh] Merge remote-tracking branch 'origin/master' into KNOX_SSO 153176450 [Prabhjyot Singh] Add more validation to KnoxAuthenticationFilter. 123349fc5 [Prabhjyot Singh] remove System.out.println, and some of the redundent lines, added more comment c79979acf [Prabhjyot Singh] Check for expired/deleted SSO cookie c9a137f76 [Prabhjyot Singh] Merge remote-tracking branch 'origin/master' into KNOX_SSO dbca0107a [Prabhjyot Singh] Add documentation for KNOX SSO 99541765d [Prabhjyot Singh] use default config 547c7b391 [Prabhjyot Singh] updating LICENSE 067a3e620 [prabhjyotsingh] fix "javax.servlet.ServletException: java.lang.NullPointerException" 564005ff5 [Prabhjyot Singh] remove "hadoop-common.version" dependency 355b475c5 [Prabhjyot Singh] knox sso Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/6caf587e Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/6caf587e Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/6caf587e Branch: refs/heads/master Commit: 6caf587e17fb6858e769fb2121b8cd66090ff759 Parents: ed46800 Author: Prabhjyot Singh <prabhjyotsi...@gmail.com> Authored: Wed Dec 20 16:21:22 2017 +0530 Committer: Prabhjyot Singh <prabhjyotsi...@gmail.com> Committed: Fri Dec 22 12:36:27 2017 +0530 ---------------------------------------------------------------------- LICENSE | 1 + conf/shiro.ini.template | 13 + docs/setup/security/shiro_authentication.md | 24 ++ zeppelin-server/pom.xml | 7 +- .../realm/jwt/JWTAuthenticationToken.java | 59 ++++ .../realm/jwt/KnoxAuthenticationFilter.java | 71 +++++ .../apache/zeppelin/realm/jwt/KnoxJwtRealm.java | 289 +++++++++++++++++++ .../zeppelin/realm/jwt/PrincipalMapper.java | 51 ++++ .../realm/jwt/PrincipalMappingException.java | 34 +++ .../realm/jwt/SimplePrincipalMapper.java | 126 ++++++++ .../org/apache/zeppelin/rest/LoginRestApi.java | 195 +++++++++---- .../zeppelin/rest/AbstractTestRestApi.java | 80 ++++- .../apache/zeppelin/rest/KnoxRestApiTest.java | 82 ++++++ zeppelin-web/src/app/app.js | 16 +- .../src/components/navbar/navbar.controller.js | 11 +- 15 files changed, 991 insertions(+), 68 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/LICENSE ---------------------------------------------------------------------- diff --git a/LICENSE b/LICENSE index c1f6f7e..f42b12c 100644 --- a/LICENSE +++ b/LICENSE @@ -257,6 +257,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (Apache 2.0) Software under ./bigquery/* was developed at Google (http://www.google.com/). Licensed under the Apache v2.0 License. (Apache 2.0) Roboto Font (https://github.com/google/roboto/) (Apache 2.0) Gson extra (https://github.com/DanySK/gson-extras) + (Apache 2.0) Nimbus JOSE+JWT (https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home) ======================================================================== BSD 3-Clause licenses http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/conf/shiro.ini.template ---------------------------------------------------------------------- diff --git a/conf/shiro.ini.template b/conf/shiro.ini.template index b306359..81b31a2 100644 --- a/conf/shiro.ini.template +++ b/conf/shiro.ini.template @@ -56,6 +56,19 @@ user3 = password4, role2 #zeppelinHubRealm.zeppelinhubUrl = https://www.zeppelinhub.com #securityManager.realms = $zeppelinHubRealm +## A same for configuring Knox SSO Realm +#knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm +#knoxJwtRealm.providerUrl = https://domain.example.com/ +#knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html +#knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout +#knoxJwtRealm.redirectParam = originalUrl +#knoxJwtRealm.cookieName = hadoop-jwt +#knoxJwtRealm.publicKeyPath = /etc/zeppelin/conf/knox-sso.pem +# +#knoxJwtRealm.groupPrincipalMapping = group.principal.mapping +#knoxJwtRealm.principalMapping = principal.mapping +#authc = org.apache.zeppelin.realm.jwt.KnoxAuthenticationFilter + sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager ### If caching of user is required then uncomment below lines http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/docs/setup/security/shiro_authentication.md ---------------------------------------------------------------------- diff --git a/docs/setup/security/shiro_authentication.md b/docs/setup/security/shiro_authentication.md index 33b67d0..a51f77e 100644 --- a/docs/setup/security/shiro_authentication.md +++ b/docs/setup/security/shiro_authentication.md @@ -210,6 +210,30 @@ securityManager.realms = $zeppelinHubRealm > Note: ZeppelinHub is not releated to Apache Zeppelin project. +### Knox SSO +[KnoxSSO](https://knox.apache.org/books/knox-0-13-0/dev-guide.html#KnoxSSO+Integration) provides an abstraction for integrating any number of authentication systems and SSO solutions and enables participating web applications to scale to those solutions more easily. Without the token exchange capabilities offered by KnoxSSO each component UI would need to integrate with each desired solution on its own. + +To enable this, apply the following change in `conf/shiro.ini` under `[main]` section. + +``` +### A sample for configuring Knox JWT Realm +knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm +## Domain of Knox SSO +knoxJwtRealm.providerUrl = https://domain.example.com/ +## Url for login +knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html +## Url for logout +knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout +knoxJwtRealm.redirectParam = originalUrl +knoxJwtRealm.cookieName = hadoop-jwt +knoxJwtRealm.publicKeyPath = /etc/zeppelin/conf/knox-sso.pem +knoxJwtRealm.groupPrincipalMapping = group.principal.mapping +knoxJwtRealm.principalMapping = principal.mapping +# This is required if KNOX SSO is enabled, to check if "knoxJwtRealm.cookieName" cookie was expired/deleted. +authc = org.apache.zeppelin.realm.jwt.KnoxAuthenticationFilter +``` + + ## Secure Cookie for Zeppelin Sessions (optional) Zeppelin can be configured to set `HttpOnly` flag in the session cookie. With this configuration, Zeppelin cookies can not be accessed via client side scripts thus preventing majority of Cross-site scripting (XSS) attacks. http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/pom.xml ---------------------------------------------------------------------- diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 925c637..296d58f 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -37,7 +37,6 @@ <!--library versions--> <commons.httpclient.version>4.3.6</commons.httpclient.version> <jersey.version>2.22.2</jersey.version> - <hadoop-common.version>2.6.0</hadoop-common.version> <quartz.scheduler.version>2.2.1</quartz.scheduler.version> <jersey.servlet.version>1.13</jersey.servlet.version> <javax.ws.rsapi.version>2.0.1</javax.ws.rsapi.version> @@ -215,6 +214,12 @@ </dependency> <dependency> + <groupId>com.nimbusds</groupId> + <artifactId>nimbus-jose-jwt</artifactId> + <version>4.41.2</version> + </dependency> + + <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>${quartz.scheduler.version}</version> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java new file mode 100644 index 0000000..2214125 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java @@ -0,0 +1,59 @@ +/* + * 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.realm.jwt; + +import org.apache.shiro.authc.AuthenticationToken; + +/** + * Created for org.apache.zeppelin.server + */ +public class JWTAuthenticationToken implements AuthenticationToken { + + private Object userId; + private String token; + + public JWTAuthenticationToken(Object userId, String token) { + this.userId = userId; + this.token = token; + } + + @Override + public Object getPrincipal() { + return getUserId(); + } + + @Override + public Object getCredentials() { + return getToken(); + } + + public Object getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java new file mode 100644 index 0000000..de19664 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java @@ -0,0 +1,71 @@ +/* + * 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.realm.jwt; + +import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; +import org.apache.shiro.web.servlet.ShiroHttpServletRequest; +import org.apache.zeppelin.utils.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; + +/** + * Created for org.apache.zeppelin.server + */ +public class KnoxAuthenticationFilter extends FormAuthenticationFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(KnoxAuthenticationFilter.class); + + protected boolean isAccessAllowed(ServletRequest request, + ServletResponse response, Object mappedValue) { + + //Check with existing shiro authentication logic + //https://github.com/apache/shiro/blob/shiro-root-1.3.2/web/src/main/java/org/apache/shiro/ + // web/filter/authc/AuthenticatingFilter.java#L123-L124 + Boolean accessAllowed = super.isAccessAllowed(request, response, mappedValue) || + !isLoginRequest(request, response) && isPermissive(mappedValue); + + if (accessAllowed) { + accessAllowed = false; + KnoxJwtRealm knoxJwtRealm = null; + for (Object realm : SecurityUtils.getRealmsList()) { + if (realm instanceof KnoxJwtRealm) { + knoxJwtRealm = (KnoxJwtRealm) realm; + break; + } + } + if (knoxJwtRealm != null) { + for (Cookie cookie : ((ShiroHttpServletRequest) request).getCookies()) { + if (cookie.getName().equals(knoxJwtRealm.getCookieName())) { + if (knoxJwtRealm.validateToken(cookie.getValue())) { + accessAllowed = true; + } + break; + } + } + } else { + LOGGER.error("Looks like this filter is enabled without enabling KnoxJwtRealm, please refer" + + " to https://zeppelin.apache.org/docs/latest/security/shiroauthentication.html" + + "#knox-sso"); + } + } + return accessAllowed; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java new file mode 100644 index 0000000..c3e9b77 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java @@ -0,0 +1,289 @@ +/* + * 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.realm.jwt; + +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jwt.SignedJWT; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.servlet.ServletException; +import org.apache.commons.io.FileUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.Groups; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAccount; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created for org.apache.zeppelin.server + */ +public class KnoxJwtRealm extends AuthorizingRealm { + + private static final Logger LOGGER = LoggerFactory.getLogger(KnoxJwtRealm.class); + + private String providerUrl; + private String redirectParam; + private String cookieName; + private String publicKeyPath; + private String login; + private String logout; + + private String principalMapping; + private String groupPrincipalMapping; + + private SimplePrincipalMapper mapper = new SimplePrincipalMapper(); + /** + * Configuration object needed by for hadoop classes + */ + private Configuration hadoopConfig; + + /** + * Hadoop Groups implementation. + */ + private Groups hadoopGroups; + + @Override + protected void onInit() { + super.onInit(); + if (principalMapping != null && !principalMapping.isEmpty() + || groupPrincipalMapping != null && !groupPrincipalMapping.isEmpty()) { + try { + mapper.loadMappingTable(principalMapping, groupPrincipalMapping); + } catch (PrincipalMappingException e) { + LOGGER.error("PrincipalMappingException in onInit", e); + } + } + + try { + hadoopConfig = new Configuration(); + hadoopGroups = new Groups(hadoopConfig); + } catch (final Exception e) { + LOGGER.error("Exception in onInit", e); + } + + } + + @Override + public boolean supports(AuthenticationToken token) { + return token != null && token instanceof JWTAuthenticationToken; + } + + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { + JWTAuthenticationToken upToken = (JWTAuthenticationToken) token; + + if (validateToken(upToken.getToken())) { + try { + SimpleAccount account = new SimpleAccount(getName(upToken), upToken.getToken(), getName()); + account.addRole(mapGroupPrincipals(getName(upToken))); + return account; + } catch (ParseException e) { + LOGGER.error("ParseException in doGetAuthenticationInfo", e); + } + } + return null; + } + + private String getName(JWTAuthenticationToken upToken) throws ParseException { + SignedJWT signed = SignedJWT.parse(upToken.getToken()); + String userName = signed.getJWTClaimsSet().getSubject(); + return userName; + } + + protected boolean validateToken(String token) { + try { + SignedJWT signed = SignedJWT.parse(token); + return validateSignature(signed); + } catch (ParseException ex) { + LOGGER.info("ParseException in validateToken", ex); + return false; + } + } + + public static RSAPublicKey parseRSAPublicKey(String pem) + throws IOException, ServletException { + String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n"; + String PEM_FOOTER = "\n-----END CERTIFICATE-----"; + String fullPem = PEM_HEADER + pem + PEM_FOOTER; + PublicKey key = null; + try { + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream is = new ByteArrayInputStream( + FileUtils.readFileToString(new File(pem)).getBytes("UTF8")); + X509Certificate cer = (X509Certificate) fact.generateCertificate(is); + key = cer.getPublicKey(); + } catch (CertificateException ce) { + String message = null; + if (pem.startsWith(PEM_HEADER)) { + message = "CertificateException - be sure not to include PEM header " + + "and footer in the PEM configuration element."; + } else { + message = "CertificateException - PEM may be corrupt"; + } + throw new ServletException(message, ce); + } catch (UnsupportedEncodingException uee) { + throw new ServletException(uee); + } catch (IOException e) { + throw new IOException(e); + } + return (RSAPublicKey) key; + } + + protected boolean validateSignature(SignedJWT jwtToken) { + boolean valid = false; + if (JWSObject.State.SIGNED == jwtToken.getState()) { + + if (jwtToken.getSignature() != null) { + + try { + RSAPublicKey publicKey = parseRSAPublicKey(publicKeyPath); + JWSVerifier verifier = new RSASSAVerifier(publicKey); + if (verifier != null && jwtToken.verify(verifier)) { + valid = true; + } + } catch (Exception e) { + LOGGER.info("Exception in validateSignature", e); + } + } + } + return valid; + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + Set<String> roles = mapGroupPrincipals(principals.toString()); + return new SimpleAuthorizationInfo(roles); + } + + /** + * Query the Hadoop implementation of {@link Groups} to retrieve groups for + * provided user. + */ + public Set<String> mapGroupPrincipals(final String mappedPrincipalName) { + /* return the groups as seen by Hadoop */ + Set<String> groups = null; + try { + hadoopGroups.refresh(); + final List<String> groupList = hadoopGroups + .getGroups(mappedPrincipalName); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("group found %s, %s", + mappedPrincipalName, groupList.toString())); + } + + groups = new HashSet<>(groupList); + + } catch (final IOException e) { + if (e.toString().contains("No groups found for user")) { + /* no groups found move on */ + LOGGER.info(String.format("No groups found for user %s", mappedPrincipalName)); + + } else { + /* Log the error and return empty group */ + LOGGER.info(String.format("errorGettingUserGroups for %s", mappedPrincipalName)); + } + groups = new HashSet(); + } + return groups; + } + + public String getProviderUrl() { + return providerUrl; + } + + public void setProviderUrl(String providerUrl) { + this.providerUrl = providerUrl; + } + + public String getRedirectParam() { + return redirectParam; + } + + public void setRedirectParam(String redirectParam) { + this.redirectParam = redirectParam; + } + + public String getCookieName() { + return cookieName; + } + + public void setCookieName(String cookieName) { + this.cookieName = cookieName; + } + + public String getPublicKeyPath() { + return publicKeyPath; + } + + public void setPublicKeyPath(String publicKeyPath) { + this.publicKeyPath = publicKeyPath; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getLogout() { + return logout; + } + + public void setLogout(String logout) { + this.logout = logout; + } + + public String getPrincipalMapping() { + return principalMapping; + } + + public void setPrincipalMapping(String principalMapping) { + this.principalMapping = principalMapping; + } + + public String getGroupPrincipalMapping() { + return groupPrincipalMapping; + } + + public void setGroupPrincipalMapping(String groupPrincipalMapping) { + this.groupPrincipalMapping = groupPrincipalMapping; + } +} + http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java new file mode 100644 index 0000000..d96efa4 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java @@ -0,0 +1,51 @@ +/* + * 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.realm.jwt; + +/*** + * + */ +public interface PrincipalMapper { + + /** + * Load the internal principal mapping table from the provided + * string value which conforms to the following semicolon delimited format: + * actual[,another-actual]=mapped;... + * @param principalMapping + */ + public abstract void loadMappingTable(String principalMapping, String groupMapping) + throws PrincipalMappingException; + + /** + * Acquire a mapped principal name from the mapping table + * as appropriate. Otherwise, the provided principalName + * will be used. + * @param principalName + * @return principal name to be used in the assertion + */ + public abstract String mapUserPrincipal(String principalName); + + /** + * Acquire array of group principal names from the mapping table + * as appropriate. Otherwise, return null. + * @param principalName + * @return group principal names to be used in the assertion + */ + public abstract String[] mapGroupPrincipal(String principalName); +} + http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java new file mode 100644 index 0000000..c3ca02f --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java @@ -0,0 +1,34 @@ +/** + * 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.realm.jwt; + +/*** + * {@link System} + */ +public class PrincipalMappingException extends Exception { + + public PrincipalMappingException(String message) { + super(message); + } + + public PrincipalMappingException(String message, Exception e) { + super(message, e); + } + + +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java new file mode 100644 index 0000000..b194810 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java @@ -0,0 +1,126 @@ +/** + * 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.realm.jwt; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.StringTokenizer; + + +/*** + * + */ +public class SimplePrincipalMapper implements PrincipalMapper { + + public HashMap<String, String[]> principalMappings = null; + public HashMap<String, String[]> groupMappings = null; + + public SimplePrincipalMapper() { + } + + /* (non-Javadoc) + * @see org.apache.hadoop.gateway.filter.PrincipalMapper#loadMappingTable(java.lang.String) + */ + @Override + public void loadMappingTable(String principalMapping, String groupMapping) + throws PrincipalMappingException { + if (principalMapping != null) { + principalMappings = parseMapping(principalMapping); + groupMappings = parseMapping(groupMapping); + } + } + + private HashMap<String, String[]> parseMapping(String mappings) + throws PrincipalMappingException { + if (mappings == null) { + return null; + } + HashMap<String, String[]> table = new HashMap<>(); + try { + StringTokenizer t = new StringTokenizer(mappings, ";"); + if (t.hasMoreTokens()) { + do { + String mapping = t.nextToken(); + String principals = mapping.substring(0, mapping.indexOf('=')); + String value = mapping.substring(mapping.indexOf('=') + 1); + String[] v = value.split(","); + String[] p = principals.split(","); + for (int i = 0; i < p.length; i++) { + table.put(p[i], v); + } + } while (t.hasMoreTokens()); + } + return table; + } catch (Exception e) { + // do not leave table in an unknown state - clear it instead + // no principal mapping will occur + table.clear(); + throw new PrincipalMappingException( + "Unable to load mappings from provided string: " + mappings + + " - no principal mapping will be provided.", e); + } + } + + /* (non-Javadoc) + * @see org.apache.hadoop.gateway.filter.PrincipalMapper#mapPrincipal(java.lang.String) + */ + @Override + public String mapUserPrincipal(String principalName) { + String[] p = null; + if (principalMappings != null) { + p = principalMappings.get(principalName); + } + if (p == null) { + return principalName; + } + + return p[0]; + } + + /* (non-Javadoc) + * @see org.apache.hadoop.gateway.filter.PrincipalMapper#mapPrincipal(java.lang.String) + */ + @Override + public String[] mapGroupPrincipal(String principalName) { + String[] groups = null; + String[] wildCardGroups = null; + + if (groupMappings != null) { + groups = groupMappings.get(principalName); + wildCardGroups = groupMappings.get("*"); + if (groups != null && wildCardGroups != null) { + groups = concat(groups, wildCardGroups); + } else if (wildCardGroups != null) { + return wildCardGroups; + } + } + + return groups; + } + + /** + * @param groups + * @param wildCardGroups + * @return + */ + public static <T> T[] concat(T[] groups, T[] wildCardGroups) { + T[] result = Arrays.copyOf(groups, groups.length + wildCardGroups.length); + System.arraycopy(wildCardGroups, 0, result, groups.length, wildCardGroups.length); + return result; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/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 index bd96684..3a084cf 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java @@ -16,25 +16,39 @@ */ package org.apache.zeppelin.rest; -import org.apache.shiro.authc.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.IncorrectCredentialsException; +import org.apache.shiro.authc.LockedAccountException; +import org.apache.shiro.authc.UnknownAccountException; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.realm.Realm; import org.apache.shiro.subject.Subject; import org.apache.zeppelin.annotation.ZeppelinApi; import org.apache.zeppelin.notebook.NotebookAuthorization; +import org.apache.zeppelin.realm.jwt.JWTAuthenticationToken; +import org.apache.zeppelin.realm.jwt.KnoxJwtRealm; 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. */ @@ -42,6 +56,7 @@ import java.util.Map; @Path("/login") @Produces("application/json") public class LoginRestApi { + private static final Logger LOG = LoggerFactory.getLogger(LoginRestApi.class); /** @@ -52,6 +67,104 @@ public class LoginRestApi { } + @GET + @ZeppelinApi + public Response getLogin(@Context HttpHeaders headers) { + JsonResponse response = null; + if (isKnoxSSOEnabled()) { + KnoxJwtRealm knoxJwtRealm = getJTWRealm(); + Cookie cookie = headers.getCookies().get(knoxJwtRealm.getCookieName()); + if (cookie != null && cookie.getValue() != null) { + Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); + if (!currentUser.isAuthenticated()) { + JWTAuthenticationToken token = new JWTAuthenticationToken(null, cookie.getValue()); + response = procedeToLogin(currentUser, token); + } + } + if (response == null) { + Map<String, String> data = new HashMap<>(); + data.put("redirectURL", constructKnoxUrl(knoxJwtRealm, knoxJwtRealm.getLogin())); + response = new JsonResponse(Status.OK, "", data); + } + return response.build(); + } + return new JsonResponse(Status.METHOD_NOT_ALLOWED).build(); + } + + private KnoxJwtRealm getJTWRealm() { + Collection realmsList = SecurityUtils.getRealmsList(); + if (realmsList != null) { + for (Iterator<Realm> iterator = realmsList.iterator(); iterator.hasNext(); ) { + Realm realm = iterator.next(); + String name = realm.getClass().getName(); + + LOG.debug("RealmClass.getName: " + name); + + if (name.equals("org.apache.zeppelin.realm.jwt.KnoxJwtRealm")) { + return (KnoxJwtRealm) realm; + } + } + } + return null; + } + + private boolean isKnoxSSOEnabled() { + Collection realmsList = SecurityUtils.getRealmsList(); + if (realmsList != null) { + for (Iterator<Realm> iterator = realmsList.iterator(); iterator.hasNext(); ) { + Realm realm = iterator.next(); + String name = realm.getClass().getName(); + LOG.debug("RealmClass.getName: " + name); + if (name.equals("org.apache.zeppelin.realm.jwt.KnoxJwtRealm")) { + return true; + } + } + } + return false; + } + + private JsonResponse procedeToLogin(Subject currentUser, AuthenticationToken token) { + JsonResponse response = null; + try { + currentUser.getSession().stop(); + currentUser.getSession(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! + + //set roles for user in NotebookAuthorization module + NotebookAuthorization.getInstance().setRoles(principal, roles); + } 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); + } + return response; + } + /** * Post Login * Returns userName & password @@ -63,7 +176,7 @@ public class LoginRestApi { @POST @ZeppelinApi public Response postLogin(@FormParam("userName") String userName, - @FormParam("password") String password) { + @FormParam("password") String password) { JsonResponse response = null; // ticket set to anonymous for anonymous user. Simplify testing. Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); @@ -71,45 +184,10 @@ public class LoginRestApi { currentUser.logout(); } if (!currentUser.isAuthenticated()) { - try { - UsernamePasswordToken token = new UsernamePasswordToken(userName, password); - // token.setRememberMe(true); - - currentUser.getSession().stop(); - currentUser.getSession(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! - - //set roles for user in NotebookAuthorization module - NotebookAuthorization.getInstance().setRoles(principal, roles); - } 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); - } + UsernamePasswordToken token = new UsernamePasswordToken(userName, password); + + response = procedeToLogin(currentUser, token); } if (response == null) { @@ -129,9 +207,26 @@ public class LoginRestApi { TicketContainer.instance.removeTicket(SecurityUtils.getPrincipal()); currentUser.getSession().stop(); currentUser.logout(); - response = new JsonResponse(Response.Status.UNAUTHORIZED, "", ""); + if (isKnoxSSOEnabled()) { + KnoxJwtRealm knoxJwtRealm = getJTWRealm(); + Map<String, String> data = new HashMap<>(); + data.put("redirectURL", constructKnoxUrl(knoxJwtRealm, knoxJwtRealm.getLogout())); + response = new JsonResponse(Status.UNAUTHORIZED, "", data); + } else { + response = new JsonResponse(Status.UNAUTHORIZED, "", ""); + + } LOG.warn(response.toString()); return response.build(); } + private String constructKnoxUrl(KnoxJwtRealm knoxJwtRealm, String path) { + StringBuilder redirectURL = new StringBuilder(knoxJwtRealm.getProviderUrl()); + redirectURL.append(path); + if (knoxJwtRealm.getRedirectParam() != null) { + redirectURL.append("?").append(knoxJwtRealm.getRedirectParam()).append("="); + } + return redirectURL.toString(); + } + } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java index 7c08365..ad0e1fd 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java @@ -17,6 +17,9 @@ package org.apache.zeppelin.rest; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; @@ -27,7 +30,6 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Pattern; - import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.PumpStreamHandler; @@ -54,10 +56,6 @@ import org.hamcrest.TypeSafeMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; - public abstract class AbstractTestRestApi { protected static final Logger LOG = LoggerFactory.getLogger(AbstractTestRestApi.class); @@ -90,6 +88,48 @@ public abstract class AbstractTestRestApi { "/api/version = anon\n" + "/** = authc"; + private static String zeppelinShiroKnox = + "[users]\n" + + "admin = password1, admin\n" + + "user1 = password2, role1, role2\n" + + "[main]\n" + + "knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm\n" + + "knoxJwtRealm.providerUrl = https://domain.example.com/\n" + + "knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html\n" + + "knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout\n" + + "knoxJwtRealm.redirectParam = originalUrl\n" + + "knoxJwtRealm.cookieName = hadoop-jwt\n" + + "knoxJwtRealm.publicKeyPath = knox-sso.pem\n" + + "authc = org.apache.zeppelin.realm.jwt.KnoxAuthenticationFilter\n" + + "sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager\n" + + "securityManager.sessionManager = $sessionManager\n" + + "securityManager.sessionManager.globalSessionTimeout = 86400000\n" + + "shiro.loginUrl = /api/login\n" + + "[roles]\n" + + "admin = *\n" + + "[urls]\n" + + "/api/version = anon\n" + + "/** = authc"; + + private static File knoxSsoPem = null; + private static String KNOX_SSO_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIChjCCAe+gAwIBAgIJALYrdDEXKwcqMA0GCSqGSIb3DQEBBQUAMIGEMQswCQYD\n" + + "VQQGEwJVUzENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVzdDEPMA0GA1UEChMG\n" + + "SGFkb29wMQ0wCwYDVQQLEwRUZXN0MTcwNQYDVQQDEy5jdHItZTEzNS0xNTEyMDY5\n" + + "MDMyOTc1LTU0NDctMDEtMDAwMDAyLmh3eC5zaXRlMB4XDTE3MTIwNDA5NTIwMFoX\n" + + "DTE4MTIwNDA5NTIwMFowgYQxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0w\n" + + "CwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRvb3AxDTALBgNVBAsTBFRlc3QxNzA1\n" + + "BgNVBAMTLmN0ci1lMTM1LTE1MTIwNjkwMzI5NzUtNTQ0Ny0wMS0wMDAwMDIuaHd4\n" + + "LnNpdGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAILFoXdz3yCy2INncYM2\n" + + "y72fYrONoQIxeeIzeJIibXLTuowSju90Q6aThSyUsQ6NEia2flnlKiCgINTNAodh\n" + + "UPUVGyGT+NMrqJzzpXAll2UUa6gIUPnXYEzYNkMIpbQOAo5BAg7YamaidbPPiT3W\n" + + "wAD1rWo3AMUY+nZJrAi4dEH5AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAB0R07/lo\n" + + "4hD+WeDEeyLTnsbFnPNXxBT1APMUmmuCjcky/19ZB8OphqTKIITONdOK/XHdjZHG\n" + + "JDOfhBkVknL42lSi45ahUAPS2PZOlQL08MbS8xajP1faterm+aHcdwJVK9dK76RB\n" + + "/bA8TFNPblPxavIOcd+R+RfFmT1YKfYIhco=\n" + + "-----END CERTIFICATE-----"; + protected static File zeppelinHome; protected static File confDir; @@ -127,7 +167,7 @@ public abstract class AbstractTestRestApi { } }; - private static void start(boolean withAuth, String testClassName) throws Exception { + private static void start(boolean withAuth, String testClassName, boolean withKnox) throws Exception { if (!wasRunning) { // copy the resources files to a temp folder zeppelinHome = new File(".."); @@ -156,7 +196,18 @@ public abstract class AbstractTestRestApi { if (!shiroIni.exists()) { shiroIni.createNewFile(); } - FileUtils.writeStringToFile(shiroIni, zeppelinShiro); + if (withKnox) { + FileUtils.writeStringToFile(shiroIni, + zeppelinShiroKnox.replaceAll("knox-sso.pem", confDir + "/knox-sso.pem")); + knoxSsoPem = new File(confDir, "knox-sso.pem"); + if (!knoxSsoPem.exists()) { + knoxSsoPem.createNewFile(); + } + FileUtils.writeStringToFile(knoxSsoPem, KNOX_SSO_PEM); + } else { + FileUtils.writeStringToFile(shiroIni, zeppelinShiro); + } + } // exclude org.apache.zeppelin.rinterpreter.* for scala 2.11 test @@ -254,13 +305,17 @@ public abstract class AbstractTestRestApi { } } } + + protected static void startUpWithKnoxEnable(String testClassName) throws Exception { + start(true, testClassName, true); + } protected static void startUpWithAuthenticationEnable(String testClassName) throws Exception { - start(true, testClassName); + start(true, testClassName, false); } protected static void startUp(String testClassName) throws Exception { - start(false, testClassName); + start(false, testClassName, false); } private static String getHostname() { @@ -383,6 +438,10 @@ public abstract class AbstractTestRestApi { } protected static GetMethod httpGet(String path, String user, String pwd) throws IOException { + return httpGet(path, user, pwd, StringUtils.EMPTY); + } + + protected static GetMethod httpGet(String path, String user, String pwd, String cookies) throws IOException { LOG.info("Connecting to {}", url + path); HttpClient httpClient = new HttpClient(); GetMethod getMethod = new GetMethod(url + path); @@ -390,6 +449,9 @@ public abstract class AbstractTestRestApi { if (userAndPasswordAreNotBlank(user, pwd)) { getMethod.setRequestHeader("Cookie", "JSESSIONID="+ getCookie(user, pwd)); } + if (!StringUtils.isBlank(cookies)) { + getMethod.setRequestHeader("Cookie", getMethod.getResponseHeader("Cookie") + ";" + cookies); + } httpClient.executeMethod(getMethod); LOG.info("{} - {}", getMethod.getStatusCode(), getMethod.getStatusText()); return getMethod; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java new file mode 100644 index 0000000..e3034e4 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java @@ -0,0 +1,82 @@ +/* + * 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 com.google.gson.Gson; +import java.io.IOException; +import java.util.Map; +import org.apache.commons.httpclient.methods.GetMethod; +import org.hamcrest.CoreMatchers; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class KnoxRestApiTest extends AbstractTestRestApi { + + private String KNOX_COOKIE = "hadoop-jwt=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6IktOT1hTU08iLCJleHAiOjE1MTM3NDU1MDd9.E2cWQo2sq75h0G_9fc9nWkL0SFMI5x_-Z0Zzr0NzQ86X4jfxliWYjr0M17Bm9GfPHRRR66s7YuYXa6DLbB4fHE0cyOoQnkfJFpU_vr1xhy0_0URc5v-Gb829b9rxuQfjKe-37hqbUdkwww2q6QQETVMvzp0rQKprUClZujyDvh0;"; + + @Rule + public ErrorCollector collector = new ErrorCollector(); + + private static final Logger LOG = LoggerFactory.getLogger(KnoxRestApiTest.class); + + Gson gson = new Gson(); + + @BeforeClass + public static void init() throws Exception { + AbstractTestRestApi.startUpWithKnoxEnable(KnoxRestApiTest.class.getSimpleName()); + } + + @AfterClass + public static void destroy() throws Exception { + AbstractTestRestApi.shutDown(); + } + + @Before + public void setUp() { + } + + + @Test + public void testThatOtherUserCanAccessNoteIfPermissionNotSet() throws IOException { + GetMethod loginWithoutCookie = httpGet("/api/security/ticket"); + Map result = gson.fromJson(loginWithoutCookie.getResponseBodyAsString(), Map.class); + collector.checkThat("Path is redirected to /login", loginWithoutCookie.getPath(), + CoreMatchers.containsString("login")); + + collector.checkThat("Path is redirected to /login", loginWithoutCookie.getPath(), + CoreMatchers.containsString("login")); + + collector.checkThat("response contains redirect URL", + ((Map) result.get("body")).get("redirectURL").toString(), CoreMatchers.equalTo( + "https://domain.example.com/gateway/knoxsso/knoxauth/login.html?originalUrl=")); + + GetMethod loginWithCookie = httpGet("/api/security/ticket", "", "", KNOX_COOKIE); + result = gson.fromJson(loginWithCookie.getResponseBodyAsString(), Map.class); + + collector.checkThat("User logged in as admin", + ((Map) result.get("body")).get("principal").toString(), CoreMatchers.equalTo("admin")); + + System.out.println(result); + } + +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/zeppelin-web/src/app/app.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index 5a4c016..ed89dd8 100644 --- a/zeppelin-web/src/app/app.js +++ b/zeppelin-web/src/app/app.js @@ -183,12 +183,16 @@ function auth () { let config = (process.env.PROD) ? {headers: { 'X-Requested-With': 'XMLHttpRequest' }} : {} return $http.get(baseUrlSrv.getRestApiBase() + '/security/ticket', config).then(function (response) { zeppelinWebApp.run(function ($rootScope) { - $rootScope.ticket = angular.fromJson(response.data).body - - $rootScope.ticket.screenUsername = $rootScope.ticket.principal - if ($rootScope.ticket.principal.indexOf('#Pac4j') === 0) { - let re = ', name=(.*?),' - $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1] + let res = angular.fromJson(response.data).body + if (res['redirectURL']) { + window.location.href = res['redirectURL'] + window.location.href + } else { + $rootScope.ticket = res + $rootScope.ticket.screenUsername = $rootScope.ticket.principal + if ($rootScope.ticket.principal.indexOf('#Pac4j') === 0) { + let re = ', name=(.*?),' + $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1] + } } }) }, function (errorResponse) { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/6caf587e/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 e92813b..6f2974e 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.js @@ -86,12 +86,19 @@ function NavCtrl ($scope, $rootScope, $http, $routeParams, $location, websocketMsgSrv.getHomeNote() } - function logout () { + function logout() { let logoutURL = baseUrlSrv.getRestApiBase() + '/login/logout' // for firefox and safari logoutURL = logoutURL.replace('//', '//false:false@') - $http.post(logoutURL).error(function () { + + $http.post(logoutURL).then(function () {}, function (response) { + if (response.data) { + let res = angular.fromJson(response.data).body + if (res['redirectURL']) { + window.location.href = res['redirectURL'] + window.location.href + } + } // force authcBasic (if configured) to logout $http.post(logoutURL).error(function () { $rootScope.userName = ''