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>