You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by ni...@apache.org on 2019/12/25 06:43:24 UTC

[kylin] branch master updated: KYLIN-4240 Kylin SSO with CAS or SAML without LDAP (#958)

This is an automated email from the ASF dual-hosted git repository.

nic pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kylin.git


The following commit(s) were added to refs/heads/master by this push:
     new 6208d6f  KYLIN-4240 Kylin SSO with CAS or SAML without LDAP (#958)
6208d6f is described below

commit 6208d6fd326f70e781b4c12fa5ab81ac7cde2d27
Author: Congling Xia <xi...@xiaomi.com>
AuthorDate: Wed Dec 25 14:43:13 2019 +0800

    KYLIN-4240 Kylin SSO with CAS or SAML without LDAP (#958)
    
    * KYLIN-4240 add additional profiles to support authn plugins in custom profile
    
    * KYLIN-4240 add CAS authn plugin in custom profile
    
    * KYLIN-4240 add SAML authn plugin (no LDAP integrated) in custom profile
---
 build/bin/kylin.sh                                 |   6 +
 .../org/apache/kylin/common/KylinConfigBase.java   |   6 +-
 pom.xml                                            |   5 +
 server-base/pom.xml                                |   4 +
 .../kylin/rest/controller/UserController.java      |   7 +-
 .../rest/security/cas/CasUserDetailsService.java   |  80 ++++++
 .../saml/SAMLSimpleUserDetailsService.java         |  64 +++++
 .../kylin/rest/service/KylinUserService.java       |  17 +-
 server/src/main/resources/applicationContext.xml   |   2 +-
 .../main/resources/kylin-security-cas-plugin.xml   | 146 ++++++++++
 .../kylin-security-saml-noldap-plugin.xml          | 305 +++++++++++++++++++++
 .../kylin/rest/service/AdminServiceTest.java       |   1 +
 webapp/app/js/controllers/auth.js                  |  56 +++-
 webapp/app/partials/login.html                     |   8 +
 14 files changed, 697 insertions(+), 10 deletions(-)

diff --git a/build/bin/kylin.sh b/build/bin/kylin.sh
index a3b8af1..64cc975 100755
--- a/build/bin/kylin.sh
+++ b/build/bin/kylin.sh
@@ -106,6 +106,12 @@ function retrieveStartCommand() {
     else
         verbose "kylin.security.profile is set to $spring_profile"
     fi
+    # the number of Spring active profiles can be greater than 1. Additional profiles
+    # can be added by setting kylin.security.additional-profiles
+    additional_security_profiles=`bash ${dir}/get-properties.sh kylin.security.additional-profiles`
+    if [[ "x${additional_security_profiles}" != "x" ]]; then
+        spring_profile="${spring_profile},${additional_security_profiles}"
+    fi
 
     retrieveDependency
 
diff --git a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
index 73b8f01..59950bc 100644
--- a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
+++ b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
@@ -1998,6 +1998,10 @@ public abstract class KylinConfigBase implements Serializable {
         return getOptional("kylin.security.acl.admin-role", "");
     }
 
+    public boolean createAdminWhenAbsent() {
+        return Boolean.parseBoolean(getOptional("kylin.security.create-admin-when-absent", FALSE));
+    }
+
     // ============================================================================
     // WEB
     // ============================================================================
@@ -2033,7 +2037,7 @@ public abstract class KylinConfigBase implements Serializable {
                 + "kylin.web.contact-mail,kylin.web.help.length,kylin.web.help.0,kylin.web.help.1,kylin.web.help.2,"
                 + "kylin.web.help.3,"
                 + "kylin.web.help,kylin.web.hide-measures,kylin.web.link-streaming-guide,kylin.server.external-acl-provider,"
-                + "kylin.security.profile,"
+                + "kylin.security.profile,kylin.security.additional-profiles,"
                 + "kylin.htrace.show-gui-trace-toggle,kylin.web.export-allow-admin,kylin.web.export-allow-other,"
                 + "kylin.cube.cubeplanner.enabled,kylin.web.dashboard-enabled,kylin.tool.auto-migrate-cube.enabled,"
                 + "kylin.job.scheduler.default,kylin.web.default-time-filter");
diff --git a/pom.xml b/pom.xml
index a3cd678..e95f888 100644
--- a/pom.xml
+++ b/pom.xml
@@ -974,6 +974,11 @@
         <artifactId>spring-security-saml2-core</artifactId>
         <version>${spring.framework.security.extensions.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.springframework.security</groupId>
+        <artifactId>spring-security-cas</artifactId>
+        <version>${spring.framework.security.version}</version>
+      </dependency>
 
       <dependency>
         <groupId>org.eclipse.jetty</groupId>
diff --git a/server-base/pom.xml b/server-base/pom.xml
index 2b5cd74..15b61d5 100644
--- a/server-base/pom.xml
+++ b/server-base/pom.xml
@@ -190,6 +190,10 @@
             <groupId>org.springframework.security.extensions</groupId>
             <artifactId>spring-security-saml2-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-cas</artifactId>
+        </dependency>
 
         <!-- spring aop -->
         <dependency>
diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java
index 2ff53f2..b07470d 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java
@@ -28,6 +28,7 @@ import javax.annotation.Nullable;
 import javax.annotation.PostConstruct;
 
 import org.apache.commons.lang.StringUtils;
+import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.metadata.MetadataConstants;
 import org.apache.kylin.rest.constant.Constant;
 import org.apache.kylin.rest.exception.BadRequestException;
@@ -78,8 +79,6 @@ public class UserController extends BasicController {
 
     private static final SimpleGrantedAuthority ALL_USERS_AUTH = new SimpleGrantedAuthority(Constant.GROUP_ALL_USERS);
 
-    private static final String ACTIVE_PROFILES_NAME = "spring.profiles.active";
-
     @Autowired
     @Qualifier("userService")
     UserService userService;
@@ -134,8 +133,8 @@ public class UserController extends BasicController {
     }
 
     private void checkProfileEditAllowed() {
-        String activeProfiles = System.getProperty(ACTIVE_PROFILES_NAME);
-        if (!"testing".equals(activeProfiles) && !"custom".equals(activeProfiles)) {
+        String securityProfile = KylinConfig.getInstanceFromEnv().getSecurityProfile();
+        if (!"testing".equals(securityProfile) && !"custom".equals(securityProfile)) {
             throw new BadRequestException("Action not allowed!");
         }
     }
diff --git a/server-base/src/main/java/org/apache/kylin/rest/security/cas/CasUserDetailsService.java b/server-base/src/main/java/org/apache/kylin/rest/security/cas/CasUserDetailsService.java
new file mode 100644
index 0000000..15863ca
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/security/cas/CasUserDetailsService.java
@@ -0,0 +1,80 @@
+/*
+ * 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.kylin.rest.security.cas;
+
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.rest.security.KylinUserManager;
+import org.apache.kylin.rest.security.ManagedUser;
+import org.jasig.cas.client.authentication.AttributePrincipal;
+import org.jasig.cas.client.validation.Assertion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.cas.userdetails.AbstractCasAssertionUserDetailsService;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * An implementation of AbstractCasAssertionUserDetailsService
+ * 
+ * It is used to load/create Kylin user when authenticated by CAS server. Users are created when they are not exists
+ * in Kylin system. Default authorities configured by {@link CasUserDetailsService#defaultAuthorities} are
+ * applied to user during creation.
+ */
+public class CasUserDetailsService extends AbstractCasAssertionUserDetailsService {
+    private static final Logger logger = LoggerFactory.getLogger(CasUserDetailsService.class);
+
+    // a default password that should not exists in user stores.
+    // since encryption is applied to the plain text, a simple value like 'NO_PASSWORD' is okay.
+    private static final String NON_EXISTENT_PASSWORD_VALUE = "NO_PASSWORD";
+
+    private String[] defaultAuthorities = {"ALL_USERS"};
+
+    public void setDefaultAuthorities(String[] defaultAuthorities) {
+        this.defaultAuthorities = defaultAuthorities;
+    }
+
+    @Override
+    protected UserDetails loadUserDetails(Assertion assertion) {
+        if (assertion == null) {
+            throw new CredentialsExpiredException("bad assertion");
+        }
+        ManagedUser user = parseUserDetails(assertion);
+        // create user if not exists
+        KylinUserManager kylinUserManager = KylinUserManager.getInstance(KylinConfig.getInstanceFromEnv());
+        ManagedUser existUser = kylinUserManager.get(user.getUsername());
+        if (existUser == null) {
+            kylinUserManager.update(user);
+        }
+        return kylinUserManager.get(user.getUsername());
+    }
+
+    protected ManagedUser parseUserDetails(Assertion assertion) {
+        AttributePrincipal principal = assertion.getPrincipal();
+        List<GrantedAuthority> grantedAuthorities = Stream.of(defaultAuthorities)
+                .map(SimpleGrantedAuthority::new)
+                .collect(Collectors.toList());
+        return new ManagedUser(principal.getName(), NON_EXISTENT_PASSWORD_VALUE, true, grantedAuthorities);
+    }
+}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/security/saml/SAMLSimpleUserDetailsService.java b/server-base/src/main/java/org/apache/kylin/rest/security/saml/SAMLSimpleUserDetailsService.java
new file mode 100644
index 0000000..e375872
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/security/saml/SAMLSimpleUserDetailsService.java
@@ -0,0 +1,64 @@
+/*
+ * 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.kylin.rest.security.saml;
+
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.rest.security.KylinUserManager;
+import org.apache.kylin.rest.security.ManagedUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.saml.SAMLCredential;
+
+/**
+ * An simple implementation of SAMLUserDetailsService.
+ * 
+ * It is used to load/create Kylin user when authenticated by SAML IdP. Users are created when they are not exists
+ * in Kylin system. Default authorities configured by {@link SAMLSimpleUserDetailsService#defaultAuthorities} are
+ * applied to user during creation. It differs from the implementation
+ * {@link org.apache.kylin.rest.security.SAMLUserDetailsService} which loads user information
+ * from LDAP.
+ */
+public class SAMLSimpleUserDetailsService implements org.springframework.security.saml.userdetails.SAMLUserDetailsService {
+    private static final Logger logger = LoggerFactory.getLogger(SAMLSimpleUserDetailsService.class);
+
+    private static final String NO_EXISTENCE_PASSWORD = "NO_PASSWORD";
+
+    private String[] defaultAuthorities = {"ALL_USERS"};
+
+    public void setDefaultAuthorities(String[] defaultAuthorities) {
+        this.defaultAuthorities = defaultAuthorities;
+    }
+
+    @Override
+    public Object loadUserBySAML(SAMLCredential samlCredential) throws UsernameNotFoundException {
+        final String userEmail = samlCredential.getAttributeAsString("email");
+        logger.debug("samlCredential.email:" + userEmail);
+        final String userName = userEmail.substring(0, userEmail.indexOf("@"));
+
+        KylinUserManager userManager = KylinUserManager.getInstance(KylinConfig.getInstanceFromEnv());
+        ManagedUser existUser = userManager.get(userName);
+        // create if not exists
+        if (existUser == null) {
+            ManagedUser user = new ManagedUser(userName, NO_EXISTENCE_PASSWORD, true, defaultAuthorities);
+            userManager.update(user);
+        }
+        return userManager.get(userName);
+    }
+}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java b/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java
index 53a611f..3c51542 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java
@@ -61,8 +61,6 @@ public class KylinUserService implements UserService {
 
     public static final Serializer<ManagedUser> SERIALIZER = new JsonSerializer<>(ManagedUser.class);
 
-    private static final String ACTIVE_PROFILES_NAME = "spring.profiles.active";
-
     private static final String ADMIN = "ADMIN";
     private static final String MODELER = "MODELER";
     private static final String ANALYST = "ANALYST";
@@ -76,7 +74,8 @@ public class KylinUserService implements UserService {
     public KylinUserService(List<User> users) throws IOException {
         pwdEncoder = new BCryptPasswordEncoder();
         synchronized (KylinUserService.class) {
-            if (!StringUtils.equals("testing", System.getProperty(ACTIVE_PROFILES_NAME))) {
+            KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
+            if (!StringUtils.equals("testing", kylinConfig.getSecurityProfile())) {
                 return;
             }
             List<ManagedUser> all = listUsers();
@@ -127,6 +126,18 @@ public class KylinUserService implements UserService {
     @PostConstruct
     public void init() throws IOException {
         aclStore = ResourceStore.getStore(KylinConfig.getInstanceFromEnv());
+
+        // check members
+        if (pwdEncoder == null) {
+            pwdEncoder = new BCryptPasswordEncoder();
+        }
+        // add default admin user if there is none
+        KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
+        if (kylinConfig.createAdminWhenAbsent() && listAdminUsers().isEmpty()) {
+            logger.info("default admin user created: username=ADMIN, password=*****");
+            createUser(new ManagedUser(ADMIN, pwdEncoder.encode(ADMIN_DEFAULT), true, Constant.ROLE_ADMIN,
+                    Constant.GROUP_ALL_USERS));
+        }
     }
 
     @Override
diff --git a/server/src/main/resources/applicationContext.xml b/server/src/main/resources/applicationContext.xml
index 523fdc2..1c29096 100644
--- a/server/src/main/resources/applicationContext.xml
+++ b/server/src/main/resources/applicationContext.xml
@@ -87,7 +87,7 @@
     <!-- Cache Config -->
     <cache:annotation-driven/>
 
-    <beans profile="ldap,saml">
+    <beans profile="!testing">
         <bean id="ehcache"
               class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
               p:configLocation="classpath:ehcache.xml" p:shared="true"/>
diff --git a/server/src/main/resources/kylin-security-cas-plugin.xml b/server/src/main/resources/kylin-security-cas-plugin.xml
new file mode 100644
index 0000000..8cc24b5
--- /dev/null
+++ b/server/src/main/resources/kylin-security-cas-plugin.xml
@@ -0,0 +1,146 @@
+<!--
+  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. See accompanying LICENSE file.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:scr="http://www.springframework.org/schema/security"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
+
+    <beans profile="authn-cas">
+        <context:annotation-config/>
+        <context:component-scan base-package="org.springframework.security.cas"/>
+
+        <!-- public resources -->
+        <scr:http security="none" pattern="/image/**"/>
+        <scr:http security="none" pattern="/css/**"/>
+        <scr:http security="none" pattern="/less/**"/>
+        <scr:http security="none" pattern="/fonts/**"/>
+        <scr:http security="none" pattern="/js/**"/>
+        <scr:http security="none" pattern="/login/**"/>
+        <scr:http security="none" pattern="/routes.json"/>
+
+        <!-- Secured Rest API urls with basic authentication -->
+        <scr:http pattern="/api/**" use-expressions="true" authentication-manager-ref="authenticationManager">
+            <scr:csrf disabled="true"/>
+            <scr:http-basic entry-point-ref="unauthorisedEntryPoint"/>
+            <scr:intercept-url pattern="/api/user/authentication*/**" access="permitAll"/>
+            <scr:intercept-url pattern="/api/query/runningQueries" access="hasRole('ROLE_ADMIN')"/>
+            <scr:intercept-url pattern="/api/query/*/stop" access="hasRole('ROLE_ADMIN')"/>
+            <scr:intercept-url pattern="/api/query*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/metadata*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/**/metrics" access="permitAll"/>
+            <scr:intercept-url pattern="/api/cache*/**" access="permitAll"/>
+            <scr:intercept-url pattern="/api/cubes/src/tables" access="hasAnyRole('ROLE_ANALYST')"/>
+            <scr:intercept-url pattern="/api/cubes*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/models*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/streaming*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/job*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/admin/public_config" access="permitAll"/>
+            <scr:intercept-url pattern="/api/projects*/*" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/admin*/**" access="hasRole('ROLE_ADMIN')"/>
+            <scr:intercept-url pattern="/api/tables/**/snapshotLocalCache/**" access="permitAll"/>
+            <scr:intercept-url pattern="/api/**" access="isAuthenticated()"/>
+
+            <scr:form-login login-page="/login"/>
+            <scr:session-management session-fixation-protection="newSession"/>
+        </scr:http>
+
+        <!-- filter on /cas/** for form login compatibility -->
+        <scr:http pattern="/cas/**" auto-config="true" entry-point-ref="casEntryPoint" use-expressions="false"
+                  authentication-manager-ref="authenticationManager">
+            <scr:csrf disabled="true"/>
+            <scr:intercept-url pattern="/cas/**" access="IS_AUTHENTICATED_FULLY"/>
+            <scr:custom-filter before="CAS_FILTER" ref="casHandleSingleLogoutFilter"/>
+            <scr:custom-filter position="CAS_FILTER" ref="casAuthenticationFilter"/>
+        </scr:http>
+        <scr:http auto-config="true" use-expressions="false" authentication-manager-ref="authenticationManager">
+            <scr:csrf disabled="true"/>
+            <scr:http-basic entry-point-ref="unauthorisedEntryPoint"/>
+            <scr:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
+            <scr:form-login login-page="/login"/>
+            <scr:logout invalidate-session="true" delete-cookies="JSESSIONID" logout-success-url="/login"
+                        logout-url="/j_spring_security_logout"/>
+        </scr:http>
+        
+        <!-- AuthenticationProvider based on username/password authentication -->
+        <bean id="kylinUserAuthProvider" class="org.apache.kylin.rest.security.KylinAuthenticationProvider">
+            <constructor-arg>
+                <bean class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
+                    <property name="userDetailsService">
+                        <bean class="org.apache.kylin.rest.service.KylinUserService"/>
+                    </property>
+                    <property name="passwordEncoder">
+                        <bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
+                    </property>
+                </bean>
+            </constructor-arg>
+        </bean>
+        <!-- AuthenticationProvider based on CAS authentication -->
+        <bean id="casAuthenticationProvider"
+              class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
+            <property name="key" value="changeit"/>
+            <property name="authenticationUserDetailsService" ref="casUserDetailsService"/>
+            <property name="serviceProperties" ref="casServiceProperties"/>
+            <property name="ticketValidator" ref="casValidator"/>
+        </bean>
+        <!-- Default AuthenticationManager which tried in order until one provider return non-null -->
+        <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
+            <constructor-arg>
+                <list>
+                    <ref bean="casAuthenticationProvider"/>
+                    <ref bean="kylinUserAuthProvider"/>
+                </list>
+            </constructor-arg>
+        </bean>
+        <bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
+            <property name="serviceProperties" ref="casServiceProperties"/>
+            <property name="authenticationManager" ref="authenticationManager"/>
+            <property name="filterProcessesUrl" value="/cas/service"/>
+        </bean>
+        
+        <!-- CAS Client related beans -->
+        <bean id="casServiceProperties" class="org.springframework.security.cas.ServiceProperties">
+            <property name="artifactParameter" value="${kylin.security.cas.artifact-param:ticket}"/>
+            <property name="serviceParameter" value="${kylin.security.cas.service-param:service}"/>
+            <property name="authenticateAllArtifacts" value="${kylin.security.cas.auth-all-artifact:false}"/>
+            <property name="sendRenew" value="${kylin.security.cas.send-renew:false}"/>
+            <property name="service" value="${kylin.server.url}/cas/service"/>
+        </bean>
+        <bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
+            <property name="loginUrl" value="${kylin.security.cas.server.login-url}"/>
+            <property name="encodeServiceUrlWithSessionId" value="true"/>
+            <property name="serviceProperties" ref="casServiceProperties"/>
+        </bean>
+        <bean id="casValidator" class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
+            <constructor-arg index="0" name="casServerUrlPrefix" value="${kylin.security.cas.server.prefix}"/>
+        </bean>
+        <bean id="casUserDetailsService" class="org.apache.kylin.rest.security.cas.CasUserDetailsService">
+            <property name="defaultAuthorities" value="${kylin.security.cas.default-groups:ALL_USERS}"/>
+        </bean>
+        <!-- handle the CAS server to ends the CAS SSO session, not used -->
+        <bean id="casHandleSingleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
+        <!-- logout in Kylin when CAS SSO session ends. redirect to CAS server logout page when accessing /cas/logout -->
+        <bean id="casRequestSingleLogoutFilter"
+              class="org.springframework.security.web.authentication.logout.LogoutFilter">
+            <constructor-arg value="${kylin.security.cas.server.logout-url}"/>
+            <constructor-arg>
+                <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
+            </constructor-arg>
+            <property name="filterProcessesUrl" value="/cas/logout"/>
+        </bean>
+    </beans>
+</beans>
\ No newline at end of file
diff --git a/server/src/main/resources/kylin-security-saml-noldap-plugin.xml b/server/src/main/resources/kylin-security-saml-noldap-plugin.xml
new file mode 100644
index 0000000..166b16d
--- /dev/null
+++ b/server/src/main/resources/kylin-security-saml-noldap-plugin.xml
@@ -0,0 +1,305 @@
+<!--
+  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. See accompanying LICENSE file.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:scr="http://www.springframework.org/schema/security"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+	http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
+	http://www.springframework.org/schema/security
+	http://www.springframework.org/schema/security/spring-security-4.2.xsd
+    http://www.springframework.org/schema/context
+    http://www.springframework.org/schema/context/spring-context.xsd">
+
+    <!-- It is nearly the same with the SAML settings in kylinSecurity.xml, except that:
+           1. LDAP authorization components have been removed; 
+           2. SAML filter intercept `/saml/**` only;
+         So that unauthenticated user will not be redirected to SAML IdP. Instead, the `/login` page will show 
+         and user can manually jump to SAML IdP by clicking "SAML Login" button. 
+    -->
+    <beans profile="authn-saml">
+        <!-- Enable auto-wiring -->
+        <context:annotation-config/>
+
+        <!-- Scan for auto-wiring classes in spring saml packages -->
+        <context:component-scan base-package="org.springframework.security.saml"/>
+
+        <!-- Unsecured pages -->
+        <scr:http security="none" pattern="/image/**"/>
+        <scr:http security="none" pattern="/css/**"/>
+        <scr:http security="none" pattern="/less/**"/>
+        <scr:http security="none" pattern="/fonts/**"/>
+        <scr:http security="none" pattern="/js/**"/>
+        <scr:http security="none" pattern="/login/**"/>
+        <scr:http security="none" pattern="/routes.json"/>
+
+        <!-- Secured Rest API urls with LDAP basic authentication -->
+        <scr:http pattern="/api/**" use-expressions="true" authentication-manager-ref="samlAuthenticationManager">
+            <scr:csrf disabled="true"/>
+            <scr:http-basic entry-point-ref="unauthorisedEntryPoint"/>
+
+            <scr:intercept-url pattern="/api/user/authentication*/**" access="permitAll"/>
+            <scr:intercept-url pattern="/api/query/runningQueries" access="hasRole('ROLE_ADMIN')"/>
+            <scr:intercept-url pattern="/api/query/*/stop" access="hasRole('ROLE_ADMIN')"/>
+            <scr:intercept-url pattern="/api/query*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/metadata*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/**/metrics" access="permitAll"/>
+            <scr:intercept-url pattern="/api/cache*/**" access="permitAll"/>
+            <scr:intercept-url pattern="/api/streaming_coordinator/**" access="permitAll"/>
+            <scr:intercept-url pattern="/api/cubes/src/tables" access="hasAnyRole('ROLE_ANALYST')"/>
+            <scr:intercept-url pattern="/api/cubes*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/models*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/streaming*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/job*/**" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/admin/public_config" access="permitAll"/>
+            <scr:intercept-url pattern="/api/projects*/*" access="isAuthenticated()"/>
+            <scr:intercept-url pattern="/api/admin*/**" access="hasRole('ROLE_ADMIN')"/>
+            <scr:intercept-url pattern="/api/tables/**/snapshotLocalCache/**" access="permitAll"/>
+            <scr:intercept-url pattern="/api/**" access="isAuthenticated()"/>
+
+            <scr:form-login login-page="/login"/>
+            <scr:session-management session-fixation-protection="newSession"/>
+        </scr:http>
+        <!-- Secured non-api urls with SAML SSO -->
+        <scr:http pattern="/saml/**" auto-config="false" authentication-manager-ref="samlAuthenticationManager"
+                  entry-point-ref="samlEntryPoint" use-expressions="false">
+            <scr:csrf disabled="true"/>
+            <scr:intercept-url pattern="/saml/**" access="IS_AUTHENTICATED_FULLY"/>
+            <scr:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
+            <scr:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
+        </scr:http>
+        <scr:http auto-config="true" use-expressions="false" authentication-manager-ref="samlAuthenticationManager">
+            <scr:csrf disabled="true"/>
+            <scr:http-basic entry-point-ref="unauthorisedEntryPoint"/>
+            <scr:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
+            <scr:form-login login-page="/login"/>
+            <scr:logout invalidate-session="true" delete-cookies="JSESSIONID" logout-success-url="/login"
+                        logout-url="/j_spring_security_logout"/>
+        </scr:http>
+
+        <!-- Authentication manager -->
+        <bean id="samlUserDetailsService" class="org.apache.kylin.rest.security.saml.SAMLSimpleUserDetailsService">
+            <property name="defaultAuthorities" value="${kylin.security.saml.default-groups:ALL_USERS}"/>
+        </bean>
+        <bean id="samlAuthenticationProvider" class="org.springframework.security.saml.SAMLAuthenticationProvider">
+            <property name="userDetails" ref="samlUserDetailsService"/>
+            <property name="consumer" ref="webSSOProfileConsumer"/>
+            <property name="hokConsumer" ref="hokWebSSOProfileConsumer"/>
+        </bean>
+        <bean id="kylinUserAuthProvider" class="org.apache.kylin.rest.security.KylinAuthenticationProvider">
+            <constructor-arg>
+                <bean class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
+                    <property name="userDetailsService">
+                        <bean class="org.apache.kylin.rest.service.KylinUserService"/>
+                    </property>
+                    <property name="passwordEncoder">
+                        <bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
+                    </property>
+                </bean>
+            </constructor-arg>
+        </bean>
+        <scr:authentication-manager id="samlAuthenticationManager">
+            <scr:authentication-provider ref="samlAuthenticationProvider"/>
+            <scr:authentication-provider ref="kylinUserAuthProvider"/>
+        </scr:authentication-manager>
+
+        <!-- Filters for processing of SAML messages -->
+        <bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
+            <scr:filter-chain-map request-matcher="ant">
+                <scr:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
+                <scr:filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter"/>
+                <scr:filter-chain pattern="/saml/metadata/**" filters="metadataGeneratorFilter"/>
+                <scr:filter-chain pattern="/saml/SSO/**" filters="samlSSOProcessingFilter"/>
+                <scr:filter-chain pattern="/saml/SLO/**" filters="samlSLOProcessingFilter"/>
+            </scr:filter-chain-map>
+        </bean>
+
+        <bean id="webSSOProfile" class="org.springframework.security.saml.websso.WebSSOProfileImpl"/>
+        <bean id="webSSOProfileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl">
+            <property name="responseSkew" value="600"/>
+        </bean>
+        <bean id="hokWebSSOProfile" class="org.springframework.security.saml.websso.WebSSOProfileHoKImpl"/>
+        <bean id="hokWebSSOProfileConsumer"
+              class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl"/>
+        <bean id="ecpWebSSOProfile" class="org.springframework.security.saml.websso.WebSSOProfileECPImpl"/>
+        <bean id="samlSLOProfile" class="org.springframework.security.saml.websso.SingleLogoutProfileImpl">
+            <property name="responseSkew" value="600"/>
+        </bean>
+
+        <bean id="parserPool" class="org.opensaml.xml.parse.StaticBasicParserPool" init-method="initialize">
+            <property name="builderFeatures">
+                <map>
+                    <entry key="http://apache.org/xml/features/dom/defer-node-expansion" value="false"/>
+                </map>
+            </property>
+        </bean>
+        <bean id="parserPoolHolder" class="org.springframework.security.saml.parser.ParserPoolHolder"/>
+        <bean id="velocityEngine" class="org.springframework.security.saml.util.VelocityFactory"
+              factory-method="getEngine"/>
+        <bean id="samlLogger" class="org.springframework.security.saml.log.SAMLDefaultLogger"/>
+        <bean id="keyManager" class="org.springframework.security.saml.key.JKSKeyManager">
+            <constructor-arg value="${kylin.security.saml.keystore-file}"/>
+            <constructor-arg type="java.lang.String" value="changeit"/>
+            <constructor-arg>
+                <map>
+                    <entry key="kylin" value="changeit"/>
+                </map>
+            </constructor-arg>
+            <constructor-arg type="java.lang.String" value="kylin"/>
+        </bean>
+        <bean id="samlBootstrap" class="org.springframework.security.saml.SAMLBootstrap"/>
+
+        <!-- SAML Bindings -->
+        <bean id="samlRedirectBinding" class="org.springframework.security.saml.processor.HTTPRedirectDeflateBinding">
+            <constructor-arg ref="parserPool"/>
+        </bean>
+        <bean id="samlPostBinding" class="org.springframework.security.saml.processor.HTTPPostBinding">
+            <constructor-arg ref="parserPool"/>
+            <constructor-arg ref="velocityEngine"/>
+        </bean>
+        <bean id="samlArtifactBinding"
+              class="org.springframework.security.saml.processor.HTTPArtifactBinding">
+            <constructor-arg ref="parserPool"/>
+            <constructor-arg ref="velocityEngine"/>
+            <constructor-arg>
+                <bean class="org.springframework.security.saml.websso.ArtifactResolutionProfileImpl">
+                    <constructor-arg>
+                        <bean class="org.apache.commons.httpclient.HttpClient">
+                            <constructor-arg>
+                                <bean class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager"/>
+                            </constructor-arg>
+                        </bean>
+                    </constructor-arg>
+                    <property name="processor">
+                        <bean class="org.springframework.security.saml.processor.SAMLProcessorImpl">
+                            <constructor-arg ref="samlSoapBinding"/>
+                        </bean>
+                    </property>
+                </bean>
+            </constructor-arg>
+        </bean>
+        <bean id="samlSoapBinding"
+              class="org.springframework.security.saml.processor.HTTPSOAP11Binding">
+            <constructor-arg ref="parserPool"/>
+        </bean>
+
+        <bean id="samlPaosBinding"
+              class="org.springframework.security.saml.processor.HTTPPAOS11Binding">
+            <constructor-arg ref="parserPool"/>
+        </bean>
+        <bean id="samlProcessor" class="org.springframework.security.saml.processor.SAMLProcessorImpl">
+            <constructor-arg>
+                <list>
+                    <ref bean="samlRedirectBinding"/>
+                    <ref bean="samlPostBinding"/>
+                    <ref bean="samlSoapBinding"/>
+                    <ref bean="samlPaosBinding"/>
+                </list>
+            </constructor-arg>
+        </bean>
+
+        <!-- SAML metadata -->
+        <bean id="metadataGeneratorFilter" class="org.springframework.security.saml.metadata.MetadataGeneratorFilter">
+            <constructor-arg>
+                <bean class="org.springframework.security.saml.metadata.MetadataGenerator">
+                    <property name="entityBaseURL" value="${kylin.security.saml.metadata-entity-base-url}"/>
+                    <property name="extendedMetadata">
+                        <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
+                            <property name="signMetadata" value="false"/>
+                            <property name="idpDiscoveryEnabled" value="true"/>
+                        </bean>
+                    </property>
+                </bean>
+            </constructor-arg>
+        </bean>
+        <bean id="metadata"
+              class="org.springframework.security.saml.metadata.CachingMetadataManager">
+            <constructor-arg>
+                <list>
+                    <!-- Example of classpath metadata with Extended Metadata -->
+                    <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
+                        <constructor-arg>
+                            <bean class="org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider">
+                                <constructor-arg>
+                                    <value type="java.io.File">${kylin.security.saml.metadata-file}</value>
+                                </constructor-arg>
+                                <property name="parserPool" ref="parserPool"/>
+                            </bean>
+                        </constructor-arg>
+                        <constructor-arg>
+                            <bean class="org.springframework.security.saml.metadata.ExtendedMetadata"/>
+                        </constructor-arg>
+                        <property name="metadataTrustCheck" value="false"/>
+                    </bean>
+                </list>
+            </constructor-arg>
+        </bean>
+
+        <!-- Log in/out -->
+        <bean id="samlContextProvider" class="org.springframework.security.saml.context.SAMLContextProviderLB">
+            <property name="scheme" value="${kylin.security.saml.context-scheme}"/>
+            <property name="serverName" value="${kylin.security.saml.context-server-name}"/>
+            <property name="serverPort" value="${kylin.security.saml.context-server-port}"/>
+            <property name="includeServerPortInRequestURL" value="false"/>
+            <property name="contextPath" value="${kylin.security.saml.context-path}"/>
+        </bean>
+
+        <bean id="samlEntryPoint" class="org.springframework.security.saml.SAMLEntryPoint">
+            <property name="defaultProfileOptions">
+                <bean class="org.springframework.security.saml.websso.WebSSOProfileOptions">
+                    <property name="includeScoping" value="false"/>
+                </bean>
+            </property>
+            <property name="webSSOprofile" ref="webSSOProfile"/>
+            <property name="webSSOprofileHoK" ref="hokWebSSOProfile"/>
+            <property name="webSSOprofileECP" ref="ecpWebSSOProfile"/>
+            <property name="contextProvider" ref="samlContextProvider"/>
+        </bean>
+        <bean id="successLogoutHandler"
+              class="org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler"/>
+        <bean id="logoutHandler"
+              class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler">
+            <property name="invalidateHttpSession" value="false"/>
+        </bean>
+
+        <!-- SAML SSO -->
+        <bean id="samlSSOProcessingFilter" class="org.springframework.security.saml.SAMLProcessingFilter">
+            <property name="authenticationManager" ref="samlAuthenticationManager"/>
+            <property name="authenticationSuccessHandler">
+                <bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
+                    <property name="defaultTargetUrl" value="/models"/>
+                </bean>
+            </property>
+            <property name="authenticationFailureHandler">
+                <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
+                    <property name="useForward" value="true"/>
+                    <property name="defaultFailureUrl" value="/login"/>
+                </bean>
+            </property>
+        </bean>
+
+        <!-- SAML SLO -->
+        <bean id="samlSLOProcessingFilter" class="org.springframework.security.saml.SAMLLogoutProcessingFilter">
+            <constructor-arg index="0" ref="successLogoutHandler"/>
+            <constructor-arg index="1" ref="logoutHandler"/>
+        </bean>
+        <bean id="samlLogoutFilter" class="org.springframework.security.saml.SAMLLogoutFilter">
+            <constructor-arg index="0" ref="successLogoutHandler"/>
+            <constructor-arg index="1" ref="logoutHandler"/>
+            <constructor-arg index="2" ref="logoutHandler"/>
+            <property name="profile" ref="samlSLOProfile"/>
+        </bean>
+    </beans>
+</beans>
diff --git a/server/src/test/java/org/apache/kylin/rest/service/AdminServiceTest.java b/server/src/test/java/org/apache/kylin/rest/service/AdminServiceTest.java
index 48fa4e5..27b59a0 100644
--- a/server/src/test/java/org/apache/kylin/rest/service/AdminServiceTest.java
+++ b/server/src/test/java/org/apache/kylin/rest/service/AdminServiceTest.java
@@ -67,6 +67,7 @@ public class AdminServiceTest extends ServiceTestBase {
                     "kylin.web.link-hadoop=\n" +
                     "kylin.web.hide-measures=RAW\n" +
                     "kylin.htrace.show-gui-trace-toggle=false\n" +
+                    "kylin.security.additional-profiles=\n" +
                     "kylin.web.export-allow-admin=true\n" +
                     "kylin.env=QA\n" +
                     "kylin.web.hive-limit=20\n" +
diff --git a/webapp/app/js/controllers/auth.js b/webapp/app/js/controllers/auth.js
index 949093e..09a50a9 100644
--- a/webapp/app/js/controllers/auth.js
+++ b/webapp/app/js/controllers/auth.js
@@ -18,11 +18,63 @@
 
 'use strict';
 
-KylinApp.controller('LoginCtrl', function ($scope, $rootScope, $location, $base64, AuthenticationService, UserService,ProjectService,ProjectModel) {
+KylinApp.controller('LoginCtrl', function ($scope, $rootScope, $location, $base64, $window, $log, AuthenticationService, UserService, ProjectService, ProjectModel, kylinConfig) {
   $scope.username = null;
   $scope.password = null;
   $scope.loading = false;
 
+  $scope.authn = null;
+
+  $scope.init = function () {
+    if (!kylinConfig.isInitialized()) {
+      kylinConfig.init().$promise.then(function (data) {
+        $scope.initCustomAuthnMethods();
+      });
+    } else {
+      $scope.initCustomAuthnMethods();
+    }
+  };
+
+  $scope.initCustomAuthnMethods = function () {
+    var profile = kylinConfig.getProperty('kylin.security.profile');
+    var additionalProfiles = kylinConfig.getProperty('kylin.security.additional-profiles');
+    if (profile !== 'custom' || !additionalProfiles) {
+      return;
+    }
+    var additions = additionalProfiles.split(',');
+    for (var i = 0; i < additions.length; i++) {
+      var prof = additions[i].trim();
+      if (!prof || !prof.startsWith('authn-'))
+        continue;
+      $scope.authn = $scope.createAuthnMethodInfo(prof.substr(6));
+      break;
+    }
+  };
+
+  $scope.createAuthnMethodInfo = function (method) {
+    if (method === 'cas') {
+      return $scope.createCASAuthnMethod();
+    }
+    if (method === 'saml') {
+      return $scope.createSAMLAuthnMethod();
+    }
+
+    $log.error('Not supported authentication method');
+    return null;
+  };
+
+  $scope.createCASAuthnMethod = function () {
+    return {label: 'CAS Login', loginEntry: 'cas/login', singleSignOut: true, logoutEntry: 'cas/logout'}
+  };
+
+  $scope.createSAMLAuthnMethod = function () {
+    return {label: 'SAML Login', loginEntry: 'saml/login', singleSignOut: true, logoutEntry: 'saml/logout'}
+  };
+
+  $scope.redirectLoginEntry = function (authn) {
+    $window.location.href = authn.loginEntry;
+  };
+
   $scope.login = function () {
     $rootScope.userAction.islogout = false;
     // set the basic authentication header that will be parsed in the next request and used to authenticate
@@ -41,4 +93,6 @@ KylinApp.controller('LoginCtrl', function ($scope, $rootScope, $location, $base6
         : "System error, please contact your administrator.";
     });
   };
+
+  $scope.init();
 });
diff --git a/webapp/app/partials/login.html b/webapp/app/partials/login.html
index faffb87..930be9b 100644
--- a/webapp/app/partials/login.html
+++ b/webapp/app/partials/login.html
@@ -55,6 +55,14 @@
                 <div class="space-4"></div>
               </form>
             </div>
+            <div class="box-footer" ng-if="authn">
+              <div>
+                <a class="btn btn-sm btn-default" ng-click="redirectLoginEntry(authn)" style="width: 100%;">
+                  <i class="ace-icon fa fa-sign-in"></i>
+                  <span class="bigger-110">{{authn.label}}</span>
+                </a>
+              </div>
+            </div>
           </div>
         </div>
       </div>