This is an automated email from the ASF dual-hosted git repository. pingsutw pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/submarine.git
The following commit(s) were added to refs/heads/master by this push: new 9e781d4b SUBMARINE-1294. Add simple type authentication for REST api 9e781d4b is described below commit 9e781d4b7302677a112b0f177668931dda52748a Author: cdmikechen <cdmikec...@hotmail.com> AuthorDate: Tue Jul 19 08:28:20 2022 +0800 SUBMARINE-1294. Add simple type authentication for REST api ### What is this PR for? Add simple type authentication for REST api. Use [pac4j](https://www.pac4j.org/index.html) for token generation and validation. Considering need to adapt to java8, this PR is using 4.5.6 instead of version pac4j 5 for the time being. ### What type of PR is it? Feature ### Todos * [x] - Add some configs for authentication * [x] - Add `SimpleFilter` * [x] - Modified some login api processing logic (no logout available yet) * [x] - Add test case ### What is the Jira issue? https://issues.apache.org/jira/projects/SUBMARINE/issues/SUBMARINE-1294 ### How should this be tested? Have added a test case in `org.apache.submarine.server.security.SubmarineAuthSimpleTest` ### Screenshots (if appropriate) No ### Questions: * Do the license files need updating? No * Are there breaking changes for older versions? Yes * Does this need new documentation? Yes(link: https://github.com/apache/submarine/pull/975) Author: cdmikechen <cdmikec...@hotmail.com> Signed-off-by: Kevin <pings...@apache.org> Closes #979 from cdmikechen/SUBMARINE-1294 and squashes the following commits: 2fff5bdc [cdmikechen] add a test user b5d9b46c [cdmikechen] get config realtime 7c2b29fe [cdmikechen] init provider 0105bac5 [cdmikechen] remove some test e0e0c25b [cdmikechen] change pc4j to 4.5.6 for adaptation of java8 909ffa9b [cdmikechen] Fist commit with pc4j 5.4.3 and test case --- dev-support/database/submarine-data.sql | 2 +- pom.xml | 2 + .../submarine/commons/utils/SubmarineConfVars.java | 7 +- submarine-server/server-core/pom.xml | 28 ++ .../apache/submarine/server/SubmarineServer.java | 14 + .../server/rest/workbench/LoginRestApi.java | 56 ++- .../submarine/server/security/SecurityFactory.java | 63 ++++ .../server/security/SecurityProvider.java | 61 +++ .../server/security/common/CommonConfig.java} | 26 +- .../server/security/common/CommonFilter.java | 46 +++ .../server/security/simple/SimpleFilter.java | 75 ++++ .../server/security/simple/SimpleLoginConfig.java | 55 +++ .../security/simple/SimpleSecurityProvider.java | 102 +++++ .../server/security/MockHttpServletRequest.java | 409 +++++++++++++++++++++ .../server/security/SubmarineAuthSimpleTest.java | 146 ++++++++ .../src/test/resources/log4j.properties | 20 + .../database/workbench/mappers/SysUserMapper.java | 6 +- .../submarine/database/mappers/SysUserMapper.xml | 10 +- 18 files changed, 1106 insertions(+), 22 deletions(-) diff --git a/dev-support/database/submarine-data.sql b/dev-support/database/submarine-data.sql index 3a767d37..423aee8c 100644 --- a/dev-support/database/submarine-data.sql +++ b/dev-support/database/submarine-data.sql @@ -53,7 +53,7 @@ INSERT INTO `sys_department` VALUES ('1bc0cd98c8d311e98edc0242ac110002','AAA','G -- ---------------------------- -- Records of sys_user -- ---------------------------- -INSERT INTO `sys_user` VALUES ('e9ca23d68d884d4ebb19d07889727dae', 'admin', 'administrator', '21232f297a57a5a743894a0e4a801fc3', 'avatar.png', '2018-12-05 00:00:00', NULL, 'd...@submarine.org', '18566666661', NULL, NULL, NULL, 1, 'admin', '2019-07-05 14:47:22', 'admin', '2019-07-05 14:47:22'); +INSERT INTO `sys_user` VALUES ('e9ca23d68d884d4ebb19d07889727dae', 'admin', 'administrator', '21232f297a57a5a743894a0e4a801fc3', 'avatar.png', '2018-12-05 00:00:00', NULL, 'd...@submarine.org', '18566666661', NULL, NULL, NULL, 0, 'admin', now(), 'admin', now()); -- ---------------------------- -- Records of team diff --git a/pom.xml b/pom.xml index 301decd0..ad2f20c8 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,8 @@ <!-- server API --> <protobuf-java.version>3.14.0</protobuf-java.version> <joda-time.version>2.10.8</joda-time.version> + <!-- pac4j --> + <pac4j.version>4.5.6</pac4j.version> </properties> <modules> diff --git a/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfVars.java b/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfVars.java index a96a8859..9d1a403e 100644 --- a/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfVars.java +++ b/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfVars.java @@ -75,7 +75,12 @@ public class SubmarineConfVars { SUBMARINE_SUBMITTER("submarine.submitter", "k8s"), SUBMARINE_SERVER_SERVICE_NAME("submarine.server.service.name", "submarine-server"), ENVIRONMENT_CONDA_MIN_VERSION("environment.conda.min.version", "4.0.1"), - ENVIRONMENT_CONDA_MAX_VERSION("environment.conda.max.version", "4.11.10"); + ENVIRONMENT_CONDA_MAX_VERSION("environment.conda.max.version", "4.11.10"), + + /* auth */ + SUBMARINE_AUTH_TYPE("submarine.auth.type", "none"), + SUBMARINE_AUTH_DEFAULT_SECRET("submarine.auth.default.secret", "SUBMARINE_SECRET_12345678901234567890"), + SUBMARINE_AUTH_MAX_AGE_ENV("submarine.auth.maxAge", 60 * 60 * 24); private String varName; @SuppressWarnings("rawtypes") diff --git a/submarine-server/server-core/pom.xml b/submarine-server/server-core/pom.xml index f891f5eb..d33ff945 100644 --- a/submarine-server/server-core/pom.xml +++ b/submarine-server/server-core/pom.xml @@ -427,6 +427,34 @@ <artifactId>joda-time</artifactId> </dependency> + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-http</artifactId> + <version>${pac4j.version}</version> + <exclusions> + <exclusion> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-jwt</artifactId> + <version>${pac4j.version}</version> + <exclusions> + <exclusion> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + </exclusion> + <exclusion> + <groupId>org.ow2.asm</groupId> + <artifactId>asm</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> <build> diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmarineServer.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmarineServer.java index 4eccbb52..44498802 100644 --- a/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmarineServer.java +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmarineServer.java @@ -20,6 +20,8 @@ package org.apache.submarine.server; import org.apache.log4j.PropertyConfigurator; import org.apache.submarine.server.rest.provider.YamlEntityProvider; +import org.apache.submarine.server.security.SecurityFactory; +import org.apache.submarine.server.security.SecurityProvider; import org.apache.submarine.server.workbench.websocket.NotebookServer; import org.apache.submarine.server.websocket.WebSocketServer; import org.eclipse.jetty.http.HttpVersion; @@ -53,6 +55,8 @@ import org.apache.submarine.commons.utils.SubmarineConfVars; import javax.inject.Inject; import javax.inject.Singleton; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -62,6 +66,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.util.EnumSet; +import java.util.Optional; public class SubmarineServer extends ResourceConfig { private static final Logger LOG = LoggerFactory.getLogger(SubmarineServer.class); @@ -201,6 +207,14 @@ public class SubmarineServer extends ResourceConfig { webApp.addServlet(new ServletHolder(RefreshServlet.class), "/user/*"); webApp.addServlet(new ServletHolder(RefreshServlet.class), "/workbench/*"); + // add security filter + Optional<SecurityProvider> securityProvider = SecurityFactory.getSecurityProvider(); + if (securityProvider.isPresent()) { + Class<Filter> filterClass = securityProvider.get().getFilterClass(); + LOG.info("Add {} to support auth", filterClass); + webApp.addFilter(filterClass, "/*", EnumSet.of(DispatcherType.REQUEST)); + } + handlers.setHandlers(new Handler[]{webApp}); return webApp; diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/workbench/LoginRestApi.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/workbench/LoginRestApi.java index 54abc57d..f385a997 100644 --- a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/workbench/LoginRestApi.java +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/workbench/LoginRestApi.java @@ -25,6 +25,8 @@ import org.apache.submarine.server.rest.workbench.annotation.SubmarineApi; import org.apache.submarine.server.database.workbench.entity.SysUserEntity; import org.apache.submarine.server.database.workbench.mappers.SysUserMapper; import org.apache.submarine.server.database.utils.MyBatisUtil; +import org.apache.submarine.server.security.common.CommonConfig; +import org.apache.submarine.server.security.simple.SimpleLoginConfig; import org.apache.submarine.server.utils.response.JsonResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +37,7 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.util.Date; import java.util.HashMap; @Path("/auth") @@ -60,16 +63,61 @@ public class LoginRestApi { SysUserMapper sysUserMapper = sqlSession.getMapper(SysUserMapper.class); sysUser = sysUserMapper.login(mapParams); if (sysUser != null) { - sysUser.setToken("mock_token"); + HashMap<String, Object> claimsMap = new HashMap<>(); + claimsMap.put("username", sysUser.getUserName()); + claimsMap.put("realName", sysUser.getRealName()); + claimsMap.put("password", sysUser.getPassword()); + claimsMap.put("avatar", sysUser.getAvatar()); + claimsMap.put("sex", sysUser.getSex()); + claimsMap.put("status", sysUser.getStatus()); + claimsMap.put("phone", sysUser.getPhone()); + claimsMap.put("email", sysUser.getEmail()); + claimsMap.put("deptCode", sysUser.getDeptCode()); + claimsMap.put("deptName", sysUser.getDeptName()); + claimsMap.put("roleCode", sysUser.getRoleCode()); + claimsMap.put("birthday", sysUser.getBirthday()); + claimsMap.put("iat", new Date().getTime()); + claimsMap.put("exp", new Date().getTime() + CommonConfig.MAX_AGE); + claimsMap.put("sub", "submarine"); + claimsMap.put("jti", sysUser.getId()); + + String token = SimpleLoginConfig.getJwtGenerator().generate(claimsMap); + sysUser.setToken(token); } else { - LOG.info("User Not Found. Please try again"); + LOG.warn("Can not find user {}", mapParams); + return new JsonResponse.Builder<>(Response.Status.UNAUTHORIZED) + .message("User Not Found. Please try again!") + .success(false) + .build(); } } catch (Exception e) { LOG.error(e.getMessage(), e); - return new JsonResponse.Builder<>(Response.Status.OK).success(false).build(); + return new JsonResponse.Builder<>(Response.Status.OK) + .message(e.getMessage()) + .success(false) + .build(); } - return new JsonResponse.Builder<SysUserEntity>(Response.Status.OK).success(true).result(sysUser).build(); + return new JsonResponse.Builder<SysUserEntity>(Response.Status.OK) + .message("Login successfully!") + .success(true) + .result(sysUser) + .build(); + } + + /** + * Get user by unique name + */ + public SysUserEntity getUserByName(String name) throws Exception { + SysUserEntity sysUser = null; + try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) { + SysUserMapper sysUserMapper = sqlSession.getMapper(SysUserMapper.class); + sysUser = sysUserMapper.getUserByUniqueName(name); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new Exception(e); + } + return sysUser; } @POST diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/SecurityFactory.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/SecurityFactory.java new file mode 100644 index 00000000..d2232443 --- /dev/null +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/SecurityFactory.java @@ -0,0 +1,63 @@ +/* + * 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.submarine.server.security; + +import org.apache.submarine.commons.utils.SubmarineConfVars; +import org.apache.submarine.commons.utils.SubmarineConfiguration; +import org.apache.submarine.server.security.simple.SimpleSecurityProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class SecurityFactory { + + private static final Logger LOG = LoggerFactory.getLogger(SecurityFactory.class); + + private static final Map<String, SecurityProvider> providerMap; + + public static SimpleSecurityProvider getSimpleSecurityProvider() { + return (SimpleSecurityProvider) providerMap.get("simple"); + } + + static { + // int provider map + providerMap = new HashMap<>(); + providerMap.put("simple", new SimpleSecurityProvider()); + } + + public static void addProvider(String name, SecurityProvider provider) { + providerMap.put(name, provider); + } + + public static Optional<SecurityProvider> getSecurityProvider() { + String authType = SubmarineConfiguration.getInstance() + .getString(SubmarineConfVars.ConfVars.SUBMARINE_AUTH_TYPE); + if (providerMap.containsKey(authType)) { + return Optional.ofNullable(providerMap.get(authType)); + } else { + LOG.warn("current auth type is {} but we can not recognize, so use none!", authType); + return Optional.empty(); + } + } + +} diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/SecurityProvider.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/SecurityProvider.java new file mode 100644 index 00000000..c775705b --- /dev/null +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/SecurityProvider.java @@ -0,0 +1,61 @@ +/* + * 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.submarine.server.security; + +import org.pac4j.core.config.Config; +import org.pac4j.core.profile.CommonProfile; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Optional; + +/** + * Provide security methods for different authentication types + */ +public interface SecurityProvider<T extends Filter, R extends CommonProfile> { + + String DEFAULT_AUTHORIZER = "isAuthenticated"; + + /** + * Get filter class + */ + Class<T> getFilterClass(); + + /** + * Get pac4j config + */ + Config getConfig(); + + /** + * Get pac4j client + */ + String getClient(HttpServletRequest httpServletRequest); + + /** + * Process authentication information and return user profile + */ + Optional<R> perform(HttpServletRequest hsRequest, HttpServletResponse hsResponse); + + /** + * Get user profile + */ + Optional<R> getProfile(HttpServletRequest hsRequest, HttpServletResponse hsResponse); +} diff --git a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/workbench/mappers/SysUserMapper.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/common/CommonConfig.java similarity index 57% copy from submarine-server/server-database/src/main/java/org/apache/submarine/server/database/workbench/mappers/SysUserMapper.java copy to submarine-server/server-core/src/main/java/org/apache/submarine/server/security/common/CommonConfig.java index 1528566a..83f1bb84 100644 --- a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/workbench/mappers/SysUserMapper.java +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/common/CommonConfig.java @@ -16,26 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.submarine.server.database.workbench.mappers; -import org.apache.ibatis.session.RowBounds; -import org.apache.submarine.server.database.workbench.entity.SysUserEntity; +package org.apache.submarine.server.security.common; -import java.util.List; -import java.util.Map; +import org.apache.submarine.commons.utils.SubmarineConfiguration; -public interface SysUserMapper { - SysUserEntity login(Map<String, String> where); +import static org.apache.submarine.commons.utils.SubmarineConfVars.ConfVars; - List<SysUserEntity> selectAll(Map<String, Object> where, RowBounds rowBounds); +public class CommonConfig { - void add(SysUserEntity sysOrg); + public static final String LOGOUT_ENDPOINT = "/auth/logout"; + public static final String AUTH_HEADER = "Authorization"; + public static final String BEARER_HEADER_PREFIX = "Bearer "; - SysUserEntity getById(String id); + public static final int MAX_AGE; - void updateBy(SysUserEntity sysUser); + static { + SubmarineConfiguration conf = SubmarineConfiguration.getInstance(); + MAX_AGE = conf.getInt(ConfVars.SUBMARINE_AUTH_MAX_AGE_ENV); + } - void deleteById(String id); - - void changePassword(SysUserEntity sysUser); } diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/common/CommonFilter.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/common/CommonFilter.java new file mode 100644 index 00000000..a0a0fdc6 --- /dev/null +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/common/CommonFilter.java @@ -0,0 +1,46 @@ +/* + * 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.submarine.server.security.common; + +import org.pac4j.core.context.JEEContext; +import org.pac4j.core.context.session.JEESessionStore; +import org.pac4j.core.context.session.SessionStore; +import org.pac4j.core.engine.DefaultCallbackLogic; +import org.pac4j.core.engine.DefaultLogoutLogic; +import org.pac4j.core.engine.DefaultSecurityLogic; +import org.pac4j.core.http.adapter.HttpActionAdapter; +import org.pac4j.core.http.adapter.JEEHttpActionAdapter; +import org.pac4j.core.profile.CommonProfile; +import org.pac4j.core.profile.UserProfile; + +public class CommonFilter { + + public static final HttpActionAdapter DEFAULT_HTTP_ACTION_ADAPTER = JEEHttpActionAdapter.INSTANCE; + + public static final DefaultCallbackLogic<CommonProfile, JEEContext> CALLBACK_LOGIC = + new DefaultCallbackLogic<>(); + + public static final DefaultSecurityLogic<UserProfile, JEEContext> SECURITY_LOGIC = + new DefaultSecurityLogic<>(); + + public static final DefaultLogoutLogic<UserProfile, JEEContext> LOGOUT_LOGIC = new DefaultLogoutLogic<>(); + + public static final SessionStore<JEEContext> SESSION_STORE = new JEESessionStore(); +} diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleFilter.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleFilter.java new file mode 100644 index 00000000..4bb91989 --- /dev/null +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleFilter.java @@ -0,0 +1,75 @@ +/* + * 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.submarine.server.security.simple; + +import org.apache.submarine.server.security.SecurityFactory; +import org.apache.submarine.server.security.common.CommonFilter; +import org.pac4j.jwt.profile.JwtProfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +/** + * Simple authentication + * Only users in submarine sys_user table can log in, and user is verified based on token + */ +public class SimpleFilter extends CommonFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleFilter.class); + + private final SimpleSecurityProvider provider; + + public SimpleFilter() { + this.provider = SecurityFactory.getSimpleSecurityProvider(); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + // check header token + Optional<JwtProfile> profile = provider.perform(httpServletRequest, httpServletResponse); + // If the token can be correctly parsed then continue processing, otherwise return 401 + if (profile.isPresent()) { + filterChain.doFilter(servletRequest, servletResponse); + } else { + httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "The token is not valid."); + } + } + + @Override + public void destroy() { + } +} diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleLoginConfig.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleLoginConfig.java new file mode 100644 index 00000000..e1e29391 --- /dev/null +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleLoginConfig.java @@ -0,0 +1,55 @@ +/* + * 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.submarine.server.security.simple; + +import org.apache.submarine.commons.utils.SubmarineConfiguration; +import org.apache.submarine.server.security.common.CommonConfig; +import org.pac4j.jwt.config.encryption.SecretEncryptionConfiguration; +import org.pac4j.jwt.config.signature.SecretSignatureConfiguration; +import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator; +import org.pac4j.jwt.profile.JwtGenerator; + +import static org.apache.submarine.commons.utils.SubmarineConfVars.ConfVars; + +public class SimpleLoginConfig extends CommonConfig { + + private static final String SUBMARINE_SECRET; + private static final JwtAuthenticator JWT_AUTHENTICATOR; + private static final JwtGenerator JWT_GENERATOR; + + static { + SubmarineConfiguration conf = SubmarineConfiguration.getInstance(); + // Generating the token requires a secret key, + // if the user does not provide the secret key, we will use the default secret key + SUBMARINE_SECRET = conf.getString(ConfVars.SUBMARINE_AUTH_DEFAULT_SECRET); + JWT_AUTHENTICATOR = new JwtAuthenticator( + new SecretSignatureConfiguration(SUBMARINE_SECRET), + new SecretEncryptionConfiguration(SUBMARINE_SECRET)); + JWT_GENERATOR = new JwtGenerator(new SecretSignatureConfiguration(SUBMARINE_SECRET)); + } + + public static JwtAuthenticator getJwtAuthenticator() { + return JWT_AUTHENTICATOR; + } + + public static JwtGenerator getJwtGenerator() { + return JWT_GENERATOR; + } +} diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleSecurityProvider.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleSecurityProvider.java new file mode 100644 index 00000000..3a351665 --- /dev/null +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/security/simple/SimpleSecurityProvider.java @@ -0,0 +1,102 @@ +/* + * 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.submarine.server.security.simple; + +import org.apache.submarine.server.security.SecurityProvider; +import org.apache.submarine.server.security.common.CommonConfig; +import org.apache.submarine.server.security.common.CommonFilter; +import org.pac4j.core.config.Config; +import org.pac4j.core.context.JEEContext; +import org.pac4j.core.matching.matcher.PathMatcher; +import org.pac4j.core.profile.ProfileManager; +import org.pac4j.core.profile.UserProfile; +import org.pac4j.http.client.direct.HeaderClient; +import org.pac4j.jwt.profile.JwtProfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collection; +import java.util.Optional; + +public class SimpleSecurityProvider implements SecurityProvider<SimpleFilter, JwtProfile> { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleSecurityProvider.class); + + private Config pac4jConfig; + + @Override + public Class<SimpleFilter> getFilterClass() { + return SimpleFilter.class; + } + + @Override + public Config getConfig() { + if (pac4jConfig != null) { + return pac4jConfig; + } + + // header client + HeaderClient headerClient = new HeaderClient(CommonConfig.AUTH_HEADER, CommonConfig.BEARER_HEADER_PREFIX, + SimpleLoginConfig.getJwtAuthenticator()); + + Config pac4jConfig = new Config(headerClient); + // skip web static resources + pac4jConfig.addMatcher("static", new PathMatcher().excludeRegex( + "^/.*(\\.map|\\.js|\\.css|\\.ico|\\.svg|\\.png|\\.html|\\.htm)$")); + // skip login rest api + pac4jConfig.addMatcher("api", new PathMatcher().excludeRegex("^/api/auth/login$")); + this.pac4jConfig = pac4jConfig; + + return pac4jConfig; + } + + @Override + public String getClient(HttpServletRequest httpServletRequest) { + return "HeaderClient"; + } + + @Override + public Optional<JwtProfile> perform(HttpServletRequest hsRequest, HttpServletResponse hsResponse) { + JEEContext context = new JEEContext(hsRequest, hsResponse, CommonFilter.SESSION_STORE); + UserProfile profile = CommonFilter.SECURITY_LOGIC.perform( + context, + pac4jConfig, + (JEEContext ctx, Collection<UserProfile> profiles, Object... parameters) -> { + if (profiles.isEmpty()) { + LOG.warn("No profiles found with default auth."); + return null; + } else { + return profiles.iterator().next(); + } + }, + CommonFilter.DEFAULT_HTTP_ACTION_ADAPTER, + getClient(hsRequest), DEFAULT_AUTHORIZER, "static,api", null); + return Optional.ofNullable((JwtProfile) profile); + } + + @Override + public Optional<JwtProfile> getProfile(HttpServletRequest hsRequest, HttpServletResponse hsResponse) { + JEEContext context = new JEEContext(hsRequest, hsResponse, CommonFilter.SESSION_STORE); + ProfileManager<JwtProfile> manager = new ProfileManager<>(context); + return manager.get(true); + } +} diff --git a/submarine-server/server-core/src/test/java/org/apache/submarine/server/security/MockHttpServletRequest.java b/submarine-server/server-core/src/test/java/org/apache/submarine/server/security/MockHttpServletRequest.java new file mode 100644 index 00000000..49d8e040 --- /dev/null +++ b/submarine-server/server-core/src/test/java/org/apache/submarine/server/security/MockHttpServletRequest.java @@ -0,0 +1,409 @@ +/* + * 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.submarine.server.security; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class MockHttpServletRequest implements HttpServletRequest { + + @Override + public String getAuthType() { + return null; + } + + @Override + public Cookie[] getCookies() { + return new Cookie[0]; + } + + @Override + public long getDateHeader(String name) { + return 0; + } + + private final HashMap<String, String> headers = new HashMap<>(); + + @Override + public String getHeader(String name) { + return headers.get(name); + } + + @Override + public Enumeration<String> getHeaders(String name) { + return Collections.enumeration(Collections.singleton(headers.get(name))); + } + + public void setHeader(String name, String value) { + headers.put(name, value); + } + + @Override + public Enumeration<String> getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String name) { + return 0; + } + + @Override + public String getMethod() { + return null; + } + + @Override + public String getPathInfo() { + return null; + } + + @Override + public String getPathTranslated() { + return null; + } + + @Override + public String getContextPath() { + return null; + } + + @Override + public String getQueryString() { + return null; + } + + @Override + public String getRemoteUser() { + return null; + } + + @Override + public boolean isUserInRole(String role) { + return false; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getRequestedSessionId() { + return null; + } + + @Override + public String getRequestURI() { + return null; + } + + private StringBuffer requestUrl; + + @Override + public StringBuffer getRequestURL() { + return requestUrl; + } + + public void setRequestURL(StringBuffer requestUrl) { + this.requestUrl = requestUrl; + } + + @Override + public String getServletPath() { + return null; + } + + @Override + public HttpSession getSession(boolean create) { + return null; + } + + @Override + public HttpSession getSession() { + return null; + } + + @Override + public String changeSessionId() { + return null; + } + + @Override + public boolean isRequestedSessionIdValid() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromUrl() { + return false; + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + return false; + } + + @Override + public void login(String username, String password) throws ServletException { + + } + + @Override + public void logout() throws ServletException { + + } + + @Override + public Collection<Part> getParts() throws IOException, ServletException { + return null; + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + return null; + } + + @Override + public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) + throws IOException, ServletException { + return null; + } + + @Override + public String getCharacterEncoding() { + return null; + } + + @Override + public void setCharacterEncoding(String env) throws UnsupportedEncodingException { + + } + + @Override + public int getContentLength() { + return 0; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return null; + } + + @Override + public String getParameter(String name) { + return null; + } + + @Override + public Enumeration<String> getParameterNames() { + return null; + } + + @Override + public String[] getParameterValues(String name) { + return new String[0]; + } + + @Override + public Map<String, String[]> getParameterMap() { + return null; + } + + @Override + public String getProtocol() { + return null; + } + + @Override + public String getScheme() { + return null; + } + + @Override + public String getServerName() { + return null; + } + + @Override + public int getServerPort() { + return 0; + } + + @Override + public BufferedReader getReader() throws IOException { + return null; + } + + @Override + public String getRemoteAddr() { + return null; + } + + @Override + public String getRemoteHost() { + return null; + } + + private Map<String, Object> attributes = new HashMap<>(); + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + @Override + public void setAttribute(String name, Object o) { + attributes.put(name, o); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public Enumeration<Locale> getLocales() { + return null; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return null; + } + + @Override + public String getRealPath(String path) { + return null; + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public String getLocalName() { + return null; + } + + @Override + public String getLocalAddr() { + return null; + } + + @Override + public int getLocalPort() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } +} diff --git a/submarine-server/server-core/src/test/java/org/apache/submarine/server/security/SubmarineAuthSimpleTest.java b/submarine-server/server-core/src/test/java/org/apache/submarine/server/security/SubmarineAuthSimpleTest.java new file mode 100644 index 00000000..b86b7b52 --- /dev/null +++ b/submarine-server/server-core/src/test/java/org/apache/submarine/server/security/SubmarineAuthSimpleTest.java @@ -0,0 +1,146 @@ +/* + * 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.submarine.server.security; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import org.apache.submarine.commons.utils.SubmarineConfVars; +import org.apache.submarine.commons.utils.SubmarineConfiguration; +import org.apache.submarine.server.api.environment.EnvironmentId; +import org.apache.submarine.server.database.workbench.entity.SysUserEntity; +import org.apache.submarine.server.rest.workbench.LoginRestApi; +import org.apache.submarine.server.rest.workbench.SysUserRestApi; +import org.apache.submarine.server.security.simple.SimpleFilter; +import org.apache.submarine.server.utils.gson.EnvironmentIdDeserializer; +import org.apache.submarine.server.utils.gson.EnvironmentIdSerializer; +import org.apache.submarine.server.utils.response.JsonResponse; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.pac4j.core.config.Config; +import org.pac4j.core.util.Pac4jConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Type; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class SubmarineAuthSimpleTest { + + private static final SubmarineConfiguration conf = SubmarineConfiguration.getInstance(); + + private static final GsonBuilder gsonBuilder = new GsonBuilder() + .registerTypeAdapter(EnvironmentId.class, new EnvironmentIdSerializer()) + .registerTypeAdapter(EnvironmentId.class, new EnvironmentIdDeserializer()); + private static final Gson gson = gsonBuilder.setDateFormat("yyyy-MM-dd HH:mm:ss").create(); + + private static final Logger LOG = LoggerFactory.getLogger(SubmarineAuthSimpleTest.class); + + private static LoginRestApi loginRestApi; + private static SysUserRestApi sysUserRestApi; + + @Before + public void before() { + conf.updateConfiguration("submarine.auth.type", "simple"); + conf.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/submarine_test?" + + "useUnicode=true&" + + "characterEncoding=UTF-8&" + + "autoReconnect=true&" + + "failOverReadOnly=false&" + + "zeroDateTimeBehavior=convertToNull&" + + "useSSL=false"); + conf.setJdbcUserName("submarine_test"); + conf.setJdbcPassword("password_test"); + loginRestApi = new LoginRestApi(); + // add a test user + sysUserRestApi = new SysUserRestApi(); + SysUserEntity user = new SysUserEntity(); + user.setUserName("test"); + user.setRealName("test"); + user.setPassword("test"); + user.setDeleted(0); + sysUserRestApi.add(user); + } + + @Test + public void testSimpleType() throws ServletException, IOException { + // test auth type config + String authType = conf.getString(SubmarineConfVars.ConfVars.SUBMARINE_AUTH_TYPE); + assertEquals(authType, "simple"); + + // test provider + Optional<SecurityProvider> providerOptional = SecurityFactory.getSecurityProvider(); + SecurityProvider provider = providerOptional.get(); + assertNotNull(provider); + assertEquals(provider.getFilterClass(), SimpleFilter.class); + Config config = provider.getConfig(); + assertTrue(config.getClients().findClient("headerClient").isPresent()); + + // test login api + String testUsrJson = "{\"username\":\"test\",\"password\":\"test\"}"; + Response loginResp = loginRestApi.login(testUsrJson); + assertEquals(loginResp.getStatus(), Response.Status.OK.getStatusCode()); + String entity = (String) loginResp.getEntity(); + Type type = new TypeToken<JsonResponse<SysUserEntity>>() { }.getType(); + JsonResponse<SysUserEntity> jsonResponse = gson.fromJson(entity, type); + String token = jsonResponse.getResult().getToken(); + LOG.info("Get user token: " + token); + + // create filter involved objects + // 1. test filter + SimpleFilter filterTest = new SimpleFilter(); + filterTest.init(null); + // 2. filter chain + FilterChain mockFilterChain = Mockito.mock(FilterChain.class); + // 3. http request + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.setRequestURL(new StringBuffer("/test/url")); + // 4. http response + HttpServletResponse mockResponse = Mockito.mock(HttpServletResponse.class); + StringWriter out = new StringWriter(); + PrintWriter printOut = new PrintWriter(out); + when(mockResponse.getWriter()).thenReturn(printOut); + + // test no header + filterTest.doFilter(mockRequest, mockResponse, mockFilterChain); + verify(mockResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "The token is not valid."); + + // test header + mockRequest.setHeader("Authorization", "Bearer " + token); + filterTest.doFilter(mockRequest, mockResponse, mockFilterChain); + verify(mockFilterChain).doFilter(mockRequest, mockResponse); + assertNotNull(mockRequest.getAttribute(Pac4jConstants.USER_PROFILES)); + } + +} diff --git a/submarine-server/server-core/src/test/resources/log4j.properties b/submarine-server/server-core/src/test/resources/log4j.properties new file mode 100644 index 00000000..bef97ca8 --- /dev/null +++ b/submarine-server/server-core/src/test/resources/log4j.properties @@ -0,0 +1,20 @@ +# 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. +log4j.rootLogger = info, stdout + +log4j.appender.stdout = org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target = System.out +log4j.appender.stdout.layout = org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n + +log4j.logger.org.apache.submarine = debug +log4j.logger.org.pac4j = debug diff --git a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/workbench/mappers/SysUserMapper.java b/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/workbench/mappers/SysUserMapper.java index 1528566a..b0493fca 100644 --- a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/workbench/mappers/SysUserMapper.java +++ b/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/workbench/mappers/SysUserMapper.java @@ -31,7 +31,11 @@ public interface SysUserMapper { void add(SysUserEntity sysOrg); - SysUserEntity getById(String id); + SysUserEntity getUserByName(Map<String, String> where); + + void activeUser(String id); + + SysUserEntity getUserByUniqueName(String name); void updateBy(SysUserEntity sysUser); diff --git a/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/SysUserMapper.xml b/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/SysUserMapper.xml index f4b6f884..49c4e9ec 100644 --- a/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/SysUserMapper.xml +++ b/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/SysUserMapper.xml @@ -50,7 +50,15 @@ </select> <select id="login" parameterType="java.util.Map" resultMap="resultMap"> - SELECT * FROM sys_user WHERE user_name = #{username} AND password = #{password} + SELECT * FROM sys_user WHERE user_name = #{username} AND password = #{password} AND deleted = 0 + </select> + + <update id="activeUser" parameterType="String"> + UPDATE sys_user SET delete = 0 where id = #{id} + </update> + + <select id="getUserByUniqueName" parameterType="String" resultMap="resultMap"> + SELECT * FROM sys_user WHERE user_name = #{mapParams.name} </select> <insert id="add" parameterType="org.apache.submarine.server.database.workbench.entity.SysUserEntity"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@submarine.apache.org For additional commands, e-mail: dev-h...@submarine.apache.org