This is an automated email from the ASF dual-hosted git repository. xxyu pushed a commit to branch kylin5 in repository https://gitbox.apache.org/repos/asf/kylin.git
commit 794a63c8542843b398e178dec7a2e1356726486b Author: fengguangyuan <qq272101...@gmail.com> AuthorDate: Fri May 26 12:20:34 2023 +0800 KYLIN-5697 Log http request details for the authentication failure --------- Co-authored-by: Guangyuan Feng <guangyuan.f...@kyligence.io> --- .../apache/kylin/rest/response/ErrorResponse.java | 4 +- .../rest/security/NUnauthorisedEntryPoint.java | 27 ++--- .../java/org/apache/kylin/rest/util/HttpUtil.java | 87 +++++++++++++++ .../rest/security/NUnauthorisedEntryPointTest.java | 1 - .../org/apache/kylin/rest/util/HttpUtilTest.java | 119 +++++++++++++++++++++ 5 files changed, 220 insertions(+), 18 deletions(-) diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/response/ErrorResponse.java b/src/common-service/src/main/java/org/apache/kylin/rest/response/ErrorResponse.java index e376e5733e..fdcf7dc727 100644 --- a/src/common-service/src/main/java/org/apache/kylin/rest/response/ErrorResponse.java +++ b/src/common-service/src/main/java/org/apache/kylin/rest/response/ErrorResponse.java @@ -21,11 +21,12 @@ package org.apache.kylin.rest.response; import static org.apache.kylin.common.exception.CommonErrorCode.FAILED_PARSE_JSON; import static org.apache.kylin.common.exception.CommonErrorCode.UNKNOWN_ERROR_CODE; +import lombok.NoArgsConstructor; import org.apache.kylin.common.exception.KylinException; +import org.apache.kylin.guava30.shaded.common.base.Throwables; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonParseException; -import org.apache.kylin.guava30.shaded.common.base.Throwables; import lombok.Data; @@ -33,6 +34,7 @@ import lombok.Data; * response to client when the return HTTP code is not 200 */ @Data +@NoArgsConstructor public class ErrorResponse extends EnvelopeResponse<Object> { //stacktrace of the exception diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/security/NUnauthorisedEntryPoint.java b/src/common-service/src/main/java/org/apache/kylin/rest/security/NUnauthorisedEntryPoint.java index 3aceb2ad5c..fc238f3510 100644 --- a/src/common-service/src/main/java/org/apache/kylin/rest/security/NUnauthorisedEntryPoint.java +++ b/src/common-service/src/main/java/org/apache/kylin/rest/security/NUnauthorisedEntryPoint.java @@ -23,9 +23,10 @@ import static org.apache.kylin.common.exception.ServerErrorCode.USER_DATA_SOURCE import static org.apache.kylin.common.exception.ServerErrorCode.USER_LOCKED; import static org.apache.kylin.common.exception.code.ErrorCodeServer.USER_LOGIN_FAILED; import static org.apache.kylin.common.exception.code.ErrorCodeServer.USER_UNAUTHORIZED; +import static org.apache.kylin.rest.util.HttpUtil.formatRequest; +import static org.apache.kylin.rest.util.HttpUtil.setErrorResponse; import java.io.IOException; -import java.io.PrintWriter; import java.net.ConnectException; import java.util.Optional; @@ -35,9 +36,6 @@ import javax.servlet.http.HttpServletResponse; import org.apache.kylin.common.exception.KylinException; import org.apache.kylin.common.msg.MsgPicker; -import org.apache.kylin.common.util.JsonUtil; -import org.apache.kylin.rest.response.ErrorResponse; -import org.springframework.http.MediaType; import org.springframework.ldap.CommunicationException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.InsufficientAuthenticationException; @@ -46,6 +44,9 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; + +@Slf4j @Component(value = "nUnauthorisedEntryPoint") public class NUnauthorisedEntryPoint implements AuthenticationEntryPoint { @@ -58,10 +59,12 @@ public class NUnauthorisedEntryPoint implements AuthenticationEntryPoint { } else if (exception instanceof InsufficientAuthenticationException) { setErrorResponse(request, response, HttpServletResponse.SC_UNAUTHORIZED, new KylinException(USER_UNAUTHORIZED)); + logRequest(request); return; } else if (exception instanceof DisabledException) { setErrorResponse(request, response, HttpServletResponse.SC_UNAUTHORIZED, new KylinException(LOGIN_FAILED, MsgPicker.getMsg().getDisabledUser())); + logRequest(request); return; } boolean present = Optional.ofNullable(exception).map(Throwable::getCause) @@ -89,17 +92,9 @@ public class NUnauthorisedEntryPoint implements AuthenticationEntryPoint { setErrorResponse(request, response, HttpServletResponse.SC_UNAUTHORIZED, new KylinException(USER_LOGIN_FAILED)); } - public void setErrorResponse(HttpServletRequest request, HttpServletResponse response, int statusCode, Exception ex) - throws IOException { - response.setStatus(statusCode); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - ErrorResponse errorResponse = new ErrorResponse(request.getRequestURL().toString(), ex); - String errorStr = JsonUtil.writeValueAsIndentString(errorResponse); - response.setCharacterEncoding("UTF-8"); - PrintWriter writer = response.getWriter(); - writer.print(errorStr); - writer.flush(); - writer.close(); + private void logRequest(HttpServletRequest request) { + if (log.isDebugEnabled()) { + log.debug("Detail http request for authentication:\n" + formatRequest(request)); + } } - } diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/util/HttpUtil.java b/src/common-service/src/main/java/org/apache/kylin/rest/util/HttpUtil.java new file mode 100644 index 0000000000..29001b6b36 --- /dev/null +++ b/src/common-service/src/main/java/org/apache/kylin/rest/util/HttpUtil.java @@ -0,0 +1,87 @@ +/* + * 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.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.kylin.common.util.JsonUtil; +import org.apache.kylin.rest.response.ErrorResponse; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServletServerHttpRequest; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class HttpUtil { + private static final String DEFAULT_CONTENT_TYPE = MediaType.APPLICATION_JSON_VALUE; + private static final Charset DEFAULT_CONTENT_CHARSET = StandardCharsets.UTF_8; + + private HttpUtil() {} + + public static String getFullRequestUrl(HttpServletRequest request) { + String url = request.getRequestURL().toString(); + if (StringUtils.isNotBlank(request.getQueryString())) { + if (url.lastIndexOf("?") > -1) { + return url + "&" + request.getQueryString(); + } else { + return url + "?" + request.getQueryString(); + } + } + return request.getRequestURL().toString(); + } + + public static void setErrorResponse(HttpServletRequest request, HttpServletResponse response, int statusCode, Exception ex) + throws IOException { + response.setStatus(statusCode); + response.setContentType(DEFAULT_CONTENT_TYPE); + ErrorResponse errorResponse = new ErrorResponse(getFullRequestUrl(request), ex); + + String errorStr = JsonUtil.writeValueAsIndentString(errorResponse); + response.setCharacterEncoding(DEFAULT_CONTENT_CHARSET.name()); + byte[] responseData = errorStr.getBytes(DEFAULT_CONTENT_CHARSET); + ServletOutputStream writer = response.getOutputStream(); + response.setContentLength(responseData.length); + writer.write(responseData, 0, responseData.length); + writer.flush(); + writer.close(); + } + + public static String formatRequest(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + sb.append("Url: ").append(getFullRequestUrl(request)).append("\n"); + sb.append("Headers: ").append(new ServletServerHttpRequest(request).getHeaders()).append("\n"); + sb.append("RemoteAddr: ").append(request.getRemoteAddr()).append("\n"); + sb.append("RemoteUser: ").append(request.getRemoteUser()).append("\n"); + sb.append("Session: ").append(formatSession(request.getSession(false))).append("\n"); + Object attr = request.getAttribute("traceId"); + if (attr != null) { + sb.append("TraceId: ").append(attr).append("\n"); + } + return sb.toString(); + } + + public static String formatSession(HttpSession session) { + return String.format("Id=%s; createTime=%s; lastAccessedTime=%s", + session.getId(), session.getCreationTime(), session.getLastAccessedTime()); + } +} diff --git a/src/common-service/src/test/java/org/apache/kylin/rest/security/NUnauthorisedEntryPointTest.java b/src/common-service/src/test/java/org/apache/kylin/rest/security/NUnauthorisedEntryPointTest.java index c89ebaada9..14b0e74711 100644 --- a/src/common-service/src/test/java/org/apache/kylin/rest/security/NUnauthorisedEntryPointTest.java +++ b/src/common-service/src/test/java/org/apache/kylin/rest/security/NUnauthorisedEntryPointTest.java @@ -78,6 +78,5 @@ public class NUnauthorisedEntryPointTest { new org.springframework.ldap.AuthenticationException(new javax.naming.AuthenticationException())) { }); Assertions.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus()); - } } diff --git a/src/common-service/src/test/java/org/apache/kylin/rest/util/HttpUtilTest.java b/src/common-service/src/test/java/org/apache/kylin/rest/util/HttpUtilTest.java new file mode 100644 index 0000000000..f5045a1b63 --- /dev/null +++ b/src/common-service/src/test/java/org/apache/kylin/rest/util/HttpUtilTest.java @@ -0,0 +1,119 @@ +/* + * 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.util; + +import static org.apache.kylin.rest.util.HttpUtil.formatRequest; +import static org.apache.kylin.rest.util.HttpUtil.formatSession; +import static org.apache.kylin.rest.util.HttpUtil.getFullRequestUrl; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.kylin.common.exception.KylinException; +import org.apache.kylin.rest.response.ErrorResponse; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class HttpUtilTest { + private static final MockHttpServletRequest REQUEST_SAMPLE = new MockHttpServletRequest(); + + private static final MockHttpSession DEFAULT_SESSION = new MockHttpSession(); + + @Before + public void init() { + REQUEST_SAMPLE.setSession(DEFAULT_SESSION); + REQUEST_SAMPLE.setMethod("Get"); + REQUEST_SAMPLE.setRequestURI("/api/projects"); + REQUEST_SAMPLE.setServerPort(8081); + REQUEST_SAMPLE.setRemoteAddr("127.0.0.1"); + REQUEST_SAMPLE.setParameter("project", "test"); + REQUEST_SAMPLE.setParameter("valid", "true"); + REQUEST_SAMPLE.setContentType(MediaType.APPLICATION_JSON_VALUE); + REQUEST_SAMPLE.setContent("{\"sample\": true}".getBytes(StandardCharsets.UTF_8)); + REQUEST_SAMPLE.setAttribute("traceId", "1"); + } + + @Test + public void testFormatUrl() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/api/projects"); + request.setQueryString("project=test"); + assertEquals("http://localhost/api/projects?project=test", getFullRequestUrl(request)); + request.setRequestURI("/api/projects?valid=true"); + assertEquals("http://localhost/api/projects?valid=true&project=test", getFullRequestUrl(request)); + } + + @Test + public void testErrorResponse() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpUtil.setErrorResponse(REQUEST_SAMPLE, response, 200, new RuntimeException("Empty")); + + assertEquals(200, response.getStatus()); + assertEquals(StandardCharsets.UTF_8.name(), response.getCharacterEncoding()); + assertEquals(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8", response.getContentType()); + assertTrue(response.getContentLength() > 0); + + ObjectMapper mapper = new ObjectMapper(); + ErrorResponse errorResponse = + mapper.reader().readValue(mapper.createParser(response.getContentAsString()), ErrorResponse.class); + + assertEquals(errorResponse.getUrl(), getFullRequestUrl(REQUEST_SAMPLE)); + assertEquals(KylinException.CODE_UNDEFINED, errorResponse.getCode()); + assertTrue(errorResponse.getStacktrace().length() > 0); + assertTrue(errorResponse.getMsg().length() > 0); + assertNull(errorResponse.getSuggestion()); + assertNull(errorResponse.getErrorCode()); + } + + @Test + public void testFormatEmptyRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setSession(DEFAULT_SESSION); + assertNotNull(request.getSession()); + + String expected = "Url: " + getFullRequestUrl(request) + "\n" + + "Headers: []\n" + + "RemoteAddr: 127.0.0.1\n" + + "RemoteUser: null\n" + + "Session: " + formatSession(request.getSession()) + "\n"; + assertEquals(expected, formatRequest(request)); + } + + @Test + public void testFormatRequest() { + assertNotNull(REQUEST_SAMPLE.getSession()); + String expected = "Url: " + getFullRequestUrl(REQUEST_SAMPLE) + "\n" + + "Headers: [Content-Type:\"application/json\", Content-Length:\"16\"]\n" + + "RemoteAddr: 127.0.0.1\n" + + "RemoteUser: null\n" + + "Session: " + formatSession(REQUEST_SAMPLE.getSession()) + "\n" + + "TraceId: 1\n"; + assertEquals(expected, formatRequest(REQUEST_SAMPLE)); + } +}