Dbeaver/pro#5864 add audit logging (#3556)

* dbeaver/pro#5864 Add audit logging

* dbeaver/pro#5864 Add audit logging

* dbeaver/pro#5864 Add audit logging

* dbeaver/pro#5864 Add audit logging

* dbeaver/pro#5864 Add audit logging

* dbeaver/pro#5864 Add audit logging

* dbeaver/pro#5864 Add audit logging

* dbeaver/pro#5864 fix review

* dbeaver/pro#5864 fix review

* dbeaver/pro#5864 fixes

* dbeaver/pro#5864 add file name upload logging

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com>
This commit is contained in:
Ruslan Musaev
2025-07-15 16:11:37 +02:00
committed by GitHub
co-authored by Evgenia Ainur
parent 5ce5f6e10d
commit bae21e5b2e
8 changed files with 125 additions and 21 deletions
@@ -29,6 +29,7 @@ Export-Package: io.cloudbeaver,
io.cloudbeaver.model,
io.cloudbeaver.model.app,
io.cloudbeaver.model.config,
io.cloudbeaver.model.apilog,
io.cloudbeaver.model.cli,
io.cloudbeaver.model.events,
io.cloudbeaver.model.fs,
@@ -0,0 +1,44 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* 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.
*/
package io.cloudbeaver.model.apilog;
import jakarta.servlet.http.HttpServletRequest;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import java.time.LocalDateTime;
import java.util.Map;
/**
* Interceptor for API call events.
* This interface allows to intercept and log API call events in a web application.
*/
public interface ApiCallInterceptor {
/**
* Intercept API call event.
*
*/
void onApiCallEvent(
@NotNull HttpServletRequest request,
@Nullable Map<String, Object> variables,
@NotNull String apiCall,
@NotNull LocalDateTime startTime,
@Nullable String errorMessage,
@NotNull String apiProtocol
);
}
@@ -57,6 +57,7 @@ public class CBAppConfig extends BaseWebAppConfiguration implements ServletAuthC
private boolean anonymousAccessEnabled;
private boolean grantConnectionsAccessToAnonymousTeam;
private boolean systemVariablesResolvingEnabled;
private boolean apiCallLoggingEnabled;
@Deprecated
private String anonymousUserRole;
private String anonymousUserTeam;
@@ -90,6 +91,7 @@ public class CBAppConfig extends BaseWebAppConfiguration implements ServletAuthC
this.linkExternalCredentialsWithUser = true;
this.grantConnectionsAccessToAnonymousTeam = false;
this.systemVariablesResolvingEnabled = false;
this.apiCallLoggingEnabled = false;
}
public CBAppConfig(CBAppConfig src) {
@@ -114,6 +116,7 @@ public class CBAppConfig extends BaseWebAppConfiguration implements ServletAuthC
this.linkExternalCredentialsWithUser = src.linkExternalCredentialsWithUser;
this.grantConnectionsAccessToAnonymousTeam = src.grantConnectionsAccessToAnonymousTeam;
this.systemVariablesResolvingEnabled = src.systemVariablesResolvingEnabled;
this.apiCallLoggingEnabled = src.apiCallLoggingEnabled;
}
@Override
@@ -354,4 +357,8 @@ public class CBAppConfig extends BaseWebAppConfiguration implements ServletAuthC
authConfiguration.clear();
}
}
public boolean isApiCallLoggingEnabled() {
return apiCallLoggingEnabled;
}
}
@@ -29,9 +29,11 @@ import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.model.apilog.ApiCallInterceptor;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.registry.WebServiceRegistry;
import io.cloudbeaver.server.HttpConstants;
import io.cloudbeaver.server.WebAppUtils;
import io.cloudbeaver.service.DBWBindingContext;
import io.cloudbeaver.service.DBWServiceBindingGraphQL;
import io.cloudbeaver.service.WebServiceBindingBase;
@@ -50,6 +52,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -67,6 +70,7 @@ public class GraphQLEndpoint extends HttpServlet {
private static final String HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
private static final String CORE_SCHEMA_FILE_NAME = "schema/schema.graphqls";
public static final String API_PROTOCOL = "GraphQL";
private final GraphQL graphQL;
private static final Gson gson = new GsonBuilder()
@@ -241,24 +245,34 @@ public class GraphQLEndpoint extends HttpServlet {
if (operationName != null) {
contextBuilder.operationName(operationName);
}
{
String apiCall = operationName;
// if (!CommonUtils.isEmpty(apiCall)) {
// if (variables != null) {
// apiCall += " (" + variables + ")";
// }
// }
String sessionId = GraphQLLoggerUtil.getSessionId(request);
String userId = GraphQLLoggerUtil.getUserId(request);
String loggerMessage = GraphQLLoggerUtil.buildLoggerMessage(sessionId, userId, variables);
if (apiCall != null) {
log.debug("API > " + apiCall + loggerMessage);
} else if (DEBUG) {
log.debug("API > " + query + loggerMessage);
String sessionId = GraphQLLoggerUtil.getSmSessionId(request);
String userId = GraphQLLoggerUtil.getUserId(request);
String loggerMessage = GraphQLLoggerUtil.buildLoggerMessage(sessionId, userId, variables);
if (operationName != null) {
log.debug("API > " + operationName + loggerMessage);
} else if (DEBUG) {
log.debug("API > " + query + loggerMessage);
}
LocalDateTime startTime = LocalDateTime.now();
ExecutionInput executionInput = contextBuilder.build();
ExecutionResult executionResult = null;
Exception executionException = null;
try {
executionResult = graphQL.execute(executionInput);
} catch (Exception e) {
executionException = e;
throw e;
} finally {
String errorMessage = null;
if (executionResult != null && executionResult.getErrors() != null && !executionResult.getErrors().isEmpty()) {
errorMessage = executionResult.getErrors().getFirst().getMessage();
} else if (executionException != null) {
errorMessage = executionException.getMessage();
}
if (WebAppUtils.getWebApplication() instanceof ApiCallInterceptor apiCallInterceptor) {
apiCallInterceptor.onApiCallEvent(request, variables, operationName, startTime, errorMessage, API_PROTOCOL);
}
}
ExecutionInput executionInput = contextBuilder.build();
ExecutionResult executionResult = graphQL.execute(executionInput);
Map<String, Object> resJSON = executionResult.toSpecification();
String resString = gson.toJson(resJSON);
@@ -267,6 +281,7 @@ public class GraphQLEndpoint extends HttpServlet {
response.getWriter().print(resString);
}
private static class WebExecutionStrategy extends AsyncExecutionStrategy {
public WebExecutionStrategy() {
@@ -335,4 +350,4 @@ public class GraphQLEndpoint extends HttpServlet {
return context.get("bindingContext");
}
}
}
@@ -44,7 +44,7 @@ public class GraphQLLoggerUtil {
return userId;
}
public static String getSessionId(HttpServletRequest request) {
public static String getSmSessionId(HttpServletRequest request) {
WebSession session = getWebSession(request);
if (session == null) {
return null;
@@ -53,7 +53,7 @@ public class GraphQLLoggerUtil {
}
@Nullable
private static WebSession getWebSession(HttpServletRequest request) {
public static WebSession getWebSession(HttpServletRequest request) {
if (request.getSession() == null) {
return null;
}
@@ -66,6 +66,7 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
}
@Override
@Nullable
public TypeDefinitionRegistry getTypeDefinition() {
return loadSchemaDefinition(getClass(), schemaFileName);
}
@@ -78,7 +79,11 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
return apiInterface.cast(proxyImpl);
}
public static TypeDefinitionRegistry loadSchemaDefinition(Class<?> theClass, String schemaPath) {
@Nullable
public static TypeDefinitionRegistry loadSchemaDefinition(@NotNull Class<?> theClass, @Nullable String schemaPath) {
if (schemaPath == null) {
return null;
}
try (InputStream schemaStream = theClass.getClassLoader().getResourceAsStream(schemaPath)) {
if (schemaStream == null) {
throw new IOException("Schema file '" + schemaPath + "' not found");
@@ -18,6 +18,7 @@ package io.cloudbeaver.service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.cloudbeaver.model.apilog.ApiCallInterceptor;
import io.cloudbeaver.model.app.ServletApplication;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.WebAppUtils;
@@ -31,6 +32,7 @@ import org.jkiss.dbeaver.model.data.json.JSONUtils;
import java.io.IOException;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.Map;
public abstract class WebServiceServletBase extends HttpServlet {
@@ -42,6 +44,7 @@ public abstract class WebServiceServletBase extends HttpServlet {
.serializeNulls()
.setPrettyPrinting()
.create();
public static final String API_PROTOCOL = "REST";
private final ServletApplication application;
@@ -60,11 +63,21 @@ public abstract class WebServiceServletBase extends HttpServlet {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Web session not found");
return;
}
LocalDateTime startTime = LocalDateTime.now();
String errorMessage = null;
try {
processServiceRequest(webSession, request, response);
} catch (Exception e) {
log.error(e);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + e.getMessage());
errorMessage = e.getMessage();
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + errorMessage);
} finally {
if (WebAppUtils.getWebApplication() instanceof ApiCallInterceptor apiCallInterceptor) {
apiCallInterceptor.onApiCallEvent(
request, getVariables(request), request.getRequestURI(), startTime, errorMessage, API_PROTOCOL
);
}
}
}
@@ -29,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Part;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase;
import org.jkiss.utils.CommonUtils;
@@ -42,6 +43,8 @@ import java.util.Map;
@MultipartConfig()
public class WebFSServlet extends WebServiceServletBase {
private static final Log log = Log.getLog(WebFSServlet.class);
private static final String PARAM_PROJECT_ID = "projectId";
private final DBWServiceFS fs;
@@ -102,4 +105,20 @@ public class WebFSServlet extends WebServiceServletBase {
CommonUtils.getRootCause(e));
}
}
@Override
protected Map<String, Object> getVariables(HttpServletRequest request) {
Map<String, Object> variables = super.getVariables(request);
try {
for (Part part : request.getParts()) {
if (part.getSubmittedFileName()!= null && !part.getSubmittedFileName().isEmpty()) {
variables.put("fileName", part.getSubmittedFileName());
break;
}
}
} catch (Exception e) {
log.debug("Failed to get fileName from request for logging", e);
}
return variables;
}
}