From 1978c38c0a79bfce43aa60ac2887b612993f0f02 Mon Sep 17 00:00:00 2001 From: Joscha Schmiedt <joscha@schmiedt.dev> Date: Wed, 19 Mar 2025 22:05:18 +0100 Subject: [PATCH] FEAT(gRPC): Add SSS support for gRPC Related to issue #362 (https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/362) --- .../grpc/ServerSideScriptingServiceImpl.java | 170 +++++++----- .../grpc/ServerSideScriptingGrpcTest.java | 244 ++++++++++++++++-- 2 files changed, 321 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/caosdb/server/grpc/ServerSideScriptingServiceImpl.java b/src/main/java/org/caosdb/server/grpc/ServerSideScriptingServiceImpl.java index 5f08b060..b42dce48 100644 --- a/src/main/java/org/caosdb/server/grpc/ServerSideScriptingServiceImpl.java +++ b/src/main/java/org/caosdb/server/grpc/ServerSideScriptingServiceImpl.java @@ -21,13 +21,14 @@ package org.caosdb.server.grpc; import com.google.protobuf.Timestamp; -import com.ibm.icu.text.Collator; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.stub.StreamObserver; +import java.io.IOException; import java.util.ArrayList; -import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.subject.Subject; import org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptRequest; @@ -41,30 +42,29 @@ import org.caosdb.server.accessControl.OneTimeAuthenticationToken; import org.caosdb.server.accessControl.SessionToken; import org.caosdb.server.entity.FileProperties; import org.caosdb.server.entity.Message; +import org.caosdb.server.scripting.ScriptingPermissions; import org.caosdb.server.scripting.ServerSideScriptingCaller; import org.caosdb.server.utils.ServerMessages; -import org.restlet.data.Form; -import org.restlet.data.Parameter; /** - * Main entry point for the server-side scripting service of the server's GRPC - * API. + * Main entry point for the server-side scripting service of the server's GRPC API. * * @author Joscha Schmiedt <joscha@schmiedt.dev> */ -public class ServerSideScriptingServiceImpl - extends ServerSideScriptingServiceImplBase { +public class ServerSideScriptingServiceImpl extends ServerSideScriptingServiceImplBase { private ServerSideScriptingCaller caller; + private static AtomicLong executionId = new AtomicLong(0); @Override public void executeServerSideScript( ExecuteServerSideScriptRequest request, - io.grpc.stub.StreamObserver<org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptResponse> responseObserver) { + io.grpc.stub.StreamObserver<org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptResponse> + responseObserver) { try { AuthInterceptor.bindSubject(); - ExecuteServerSideScriptResponse response = executeScript(request); + ExecuteServerSideScriptResponse response = callScript(request); responseObserver.onNext(response); responseObserver.onCompleted(); @@ -73,24 +73,27 @@ public class ServerSideScriptingServiceImpl } } - public int callScript(List<String> commandLine, Integer timeoutMs, - List<FileProperties> files, Object authToken) - throws Message { - this.caller = new ServerSideScriptingCaller( - commandLine.toArray(new String[commandLine.size()]), timeoutMs, files, - authToken); - return caller.invoke(); + private long incrementAndGetExecutionId() { + return executionId.incrementAndGet(); } + /** + * Converts the ExecuteServerSideScriptRequest to a command line argument list. + * + * @param request the request containing the script filename and arguments + * @return the command line argument list + * @throws IllegalArgumentException if the script filename is missing + */ public static ArrayList<String> request2CommandLine(ExecuteServerSideScriptRequest request) - throws Message { + throws IllegalArgumentException { if (request.getScriptFilename().length() == 0) { - throw ServerMessages.SERVER_SIDE_SCRIPT_MISSING_CALL; + throw new IllegalArgumentException("Script filename is missing in the request."); } - ArrayList<String> commandLine = new ArrayList<>( - 1 + request.getPositionalArgumentsCount() + request.getNamedArgumentsCount()); + ArrayList<String> commandLine = + new ArrayList<>( + 1 + request.getPositionalArgumentsCount() + request.getNamedArgumentsCount()); // first item is the script filename commandLine.add(request.getScriptFilename()); @@ -114,20 +117,20 @@ public class ServerSideScriptingServiceImpl // TODO: isAnonymous, getUser and generateAuthToken are copy-pasted from // elsewhere. Consider refactoring. ///////////////////////////////////////////////////////////////////////////////////////// - public boolean isAnonymous() { + private static boolean isAnonymous() { return AuthenticationUtils.isAnonymous(getUser()); } - public Subject getUser() { + private static Subject getUser() { Subject ret = SecurityUtils.getSubject(); return ret; } - private String[] constructCallStringArray(ArrayList<String> commandLine) { + private static String[] constructCallStringArray(ArrayList<String> commandLine) { return commandLine.toArray(new String[commandLine.size()]); } - public Object generateAuthToken(String call) { + private static Object generateAuthToken(String call) { String purpose = "SCRIPTING:EXECUTE:" + call; Object authtoken = OneTimeAuthenticationToken.generateForPurpose(purpose, getUser()); if (authtoken != null || isAnonymous()) { @@ -135,53 +138,87 @@ public class ServerSideScriptingServiceImpl } return SessionToken.generate(getUser()); } + + private static void checkExecutionPermission(Subject user, String call) + throws AuthorizationException { + user.checkPermission(ScriptingPermissions.PERMISSION_EXECUTION(call)); + } + ///////////////////////////////////////////////////////////////////////////////////////// - private ExecuteServerSideScriptResponse executeScript(ExecuteServerSideScriptRequest request) throws Message { + public ExecuteServerSideScriptResponse callScript(ExecuteServerSideScriptRequest request) + throws Message { + + long startTimeMillis = System.currentTimeMillis(); + Timestamp startTime = Timestamp.newBuilder().setSeconds(startTimeMillis / 1000).build(); + + checkExecutionPermission(getUser(), request.getScriptFilename()); String[] call = constructCallStringArray(request2CommandLine(request)); - // construct caller and invoke (using an empty file list for now) - Integer timeoutMs = request.getTimeoutMs() != 0 ? (int) request.getTimeoutMs() : null; + Integer timeoutMs = request.getTimeoutMs() != 0 ? (int) request.getTimeoutMs() : -1; + + if (request.getScriptFilesCount() > 0) { + throw new UnsupportedOperationException( + "Uploading files for scripting via gRPC is not supported yet."); + } + + // auth token Object authToken = request.getAuthToken(); if (authToken == null) { authToken = generateAuthToken(request.getScriptFilename()); } - caller = new ServerSideScriptingCaller(call, timeoutMs, new ArrayList<FileProperties>(), authToken); - // request.getCommandLineList(), request.getTimeoutMs(), - // request.getFilesList(), request.getAuthToken()); - // caller.invoke(); + // invoke script + caller = + new ServerSideScriptingCaller(call, timeoutMs, new ArrayList<FileProperties>(), authToken); + int returnCode = caller.invoke(); + + // construct response + String scriptExecutionId = String.valueOf(incrementAndGetExecutionId()); + String scriptFilename = request.getScriptFilename(); + ServerSideScriptExecutionResult result = + ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_SUCCESS; - // response - String scriptExecutionId = ""; - String scriptFilename = ""; - ServerSideScriptExecutionResult result = ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_GENERAL_FAILURE; - int returnCode = 1; String stdout = ""; + try { + stdout = caller.getStdOut(); + } catch (final IOException e) { + stdout = "Error retreiving stdout"; + result = ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_GENERAL_FAILURE; + } + String stderr = ""; - Timestamp startTime = Timestamp.newBuilder().setSeconds(0).setNanos(0).build(); - Timestamp endTime = Timestamp.newBuilder().setSeconds(0).setNanos(0).build(); - int duration_ms = 0; - - final ExecuteServerSideScriptResponse response = ExecuteServerSideScriptResponse.newBuilder() - .setScriptExecutionId(ServerSideScriptExecutionId.newBuilder() - .setScriptExecutionId(scriptExecutionId) - .build()) - .setCall(scriptFilename) - .setResult(result) - .setReturnCode(returnCode) - .setStdout(stdout) - .setStderr(stderr) - .setStartTime(startTime) - .setEndTime(endTime) - .setDurationMs(duration_ms) - .build(); + try { + stderr = caller.getStdErr(); + } catch (final IOException e) { + stderr = "Error retrieving stderr"; + result = ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_GENERAL_FAILURE; + } + + long endTimeMillis = System.currentTimeMillis(); + Timestamp endTime = Timestamp.newBuilder().setSeconds(endTimeMillis / 1000).build(); + int duration_ms = (int) (endTimeMillis - startTimeMillis); + + final ExecuteServerSideScriptResponse response = + ExecuteServerSideScriptResponse.newBuilder() + .setScriptExecutionId( + ServerSideScriptExecutionId.newBuilder() + .setScriptExecutionId(scriptExecutionId) + .build()) + .setCall(scriptFilename) + .setResult(result) + .setReturnCode(returnCode) + .setStdout(stdout) + .setStderr(stderr) + .setStartTime(startTime) + .setEndTime(endTime) + .setDurationMs(duration_ms) + .build(); return response; } - public static void handleException(StreamObserver<?> responseObserver, - Exception e) { + public static void handleException(StreamObserver<?> responseObserver, Exception e) { String description = e.getMessage(); if (description == null || description.isBlank()) { description = "Unknown Error. Please Report!"; @@ -189,26 +226,25 @@ public class ServerSideScriptingServiceImpl if (e instanceof UnauthorizedException) { Subject subject = SecurityUtils.getSubject(); if (AuthenticationUtils.isAnonymous(subject)) { - responseObserver.onError( - new StatusException(AuthInterceptor.PLEASE_LOG_IN)); + responseObserver.onError(new StatusException(AuthInterceptor.PLEASE_LOG_IN)); return; } else { - responseObserver.onError(new StatusException( - Status.PERMISSION_DENIED.withCause(e).withDescription( - description))); + responseObserver.onError( + new StatusException( + Status.PERMISSION_DENIED.withCause(e).withDescription(description))); return; } - } else if (e == ServerMessages.ROLE_DOES_NOT_EXIST || - e == ServerMessages.ACCOUNT_DOES_NOT_EXIST) { - responseObserver.onError(new StatusException( - Status.NOT_FOUND.withDescription(description).withCause(e))); + } else if (e == ServerMessages.ROLE_DOES_NOT_EXIST + || e == ServerMessages.ACCOUNT_DOES_NOT_EXIST) { + responseObserver.onError( + new StatusException(Status.NOT_FOUND.withDescription(description).withCause(e))); return; } // TODO: SERVER_SIDE_DOES_NOT_EXIST, SERVER_SIDE_SCRIPT_NOT_EXECUTABLE, // SERVER_SIDE_SCRIPT_ERROR, SERVER_SIDE_SCRIPT_SETUP_ERROR, // SERVER_SIDE_SCRIPT_TIMEOUT, SERVER_SIDE_SCRIPT_MISSING_CALL e.printStackTrace(); - responseObserver.onError(new StatusException( - Status.UNKNOWN.withDescription(description).withCause(e))); + responseObserver.onError( + new StatusException(Status.UNKNOWN.withDescription(description).withCause(e))); } } diff --git a/src/test/java/org/caosdb/server/grpc/ServerSideScriptingGrpcTest.java b/src/test/java/org/caosdb/server/grpc/ServerSideScriptingGrpcTest.java index 7265dbdf..7e24f9d4 100644 --- a/src/test/java/org/caosdb/server/grpc/ServerSideScriptingGrpcTest.java +++ b/src/test/java/org/caosdb/server/grpc/ServerSideScriptingGrpcTest.java @@ -22,38 +22,230 @@ package org.caosdb.server.grpc; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; import org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptRequest; +import org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptResponse; import org.caosdb.api.scripting.v1alpha1.NamedArgument; -// Python Test Case -// response = run_server_side_script("my_script.py", -// "pos0", -// "pos1", -// option1="val1", -// option2="val2", -// files={"-Ofile": "test_file.txt"}) -// assert response.stderr is None -// assert response.code == 0 -// assert response.call == ('my_script.py ' -// '--option1=val1 --option2=val2 --file=.upload_files/test_file.txt ' -// 'pos0 pos1') +import org.caosdb.api.scripting.v1alpha1.ServerSideScriptExecutionResult; +import org.caosdb.server.CaosDBServer; +import org.caosdb.server.ServerProperties; +import org.caosdb.server.accessControl.AnonymousAuthenticationToken; +import org.caosdb.server.accessControl.CredentialsValidator; +import org.caosdb.server.accessControl.Principal; +import org.caosdb.server.accessControl.Role; +import org.caosdb.server.accessControl.UserSources; +import org.caosdb.server.database.BackendTransaction; +import org.caosdb.server.database.access.Access; +import org.caosdb.server.database.backend.interfaces.RetrievePasswordValidatorImpl; +import org.caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl; +import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl; +import org.caosdb.server.database.backend.interfaces.RetrieveUserImpl; +import org.caosdb.server.database.exceptions.TransactionException; +import org.caosdb.server.database.misc.TransactionBenchmark; +import org.caosdb.server.database.proto.ProtoUser; +import org.caosdb.server.entity.Message; +import org.caosdb.server.permissions.PermissionRule; +import org.caosdb.server.scripting.ScriptingPermissions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class ServerSideScriptingGrpcTest { + + public static class RetrieveRoleMockup implements RetrieveRoleImpl { + + public RetrieveRoleMockup(Access a) {} + + @Override + public Role retrieve(String role) throws TransactionException { + Role ret = new Role(); + ret.name = "anonymous"; + ret.description = "bla"; + return ret; + } + + @Override + public TransactionBenchmark getBenchmark() { + return null; + } + + @Override + public void setTransactionBenchmark(TransactionBenchmark b) {} + } + + public static class RetrievePermissionRules implements RetrievePermissionRulesImpl { + + public RetrievePermissionRules(Access a) {} + + @Override + public HashSet<PermissionRule> retrievePermissionRule(String role) throws TransactionException { + HashSet<PermissionRule> result = new HashSet<>(); + result.add( + new PermissionRule( + true, false, ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok"))); + return result; + } + + @Override + public TransactionBenchmark getBenchmark() { + return null; + } + + @Override + public void setTransactionBenchmark(TransactionBenchmark b) {} + } + + public static class RetrieveUserMockUp implements RetrieveUserImpl { + + public RetrieveUserMockUp(Access a) {} + + @Override + public ProtoUser execute(Principal principal) throws TransactionException { + return new ProtoUser(); + } + + @Override + public TransactionBenchmark getBenchmark() { + return null; + } + + @Override + public void setTransactionBenchmark(TransactionBenchmark b) {} + } + + public static class RetrievePasswordValidator implements RetrievePasswordValidatorImpl { + + public RetrievePasswordValidator(Access a) {} + + @Override + public CredentialsValidator<String> execute(String name) throws TransactionException { + return new CredentialsValidator<String>() { + @Override + public boolean isValid(String credential) { + return true; + } + }; + } + + @Override + public void setTransactionBenchmark(TransactionBenchmark b) {} + + @Override + public TransactionBenchmark getBenchmark() { + return null; + } + } + + @BeforeAll + public static void setupShiro() throws IOException { + CaosDBServer.initServerProperties(); + CaosDBServer.initShiro(); + + BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class); + BackendTransaction.setImpl(RetrievePermissionRulesImpl.class, RetrievePermissionRules.class); + BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUserMockUp.class); + BackendTransaction.setImpl( + RetrievePasswordValidatorImpl.class, RetrievePasswordValidator.class); + + UserSources.getDefaultRealm(); + } + @Test - public void testExecuteServerSideScriptRequestToCommandline() { - ExecuteServerSideScriptRequest request = ExecuteServerSideScriptRequest.newBuilder() - .setScriptFilename("my_script.py") - .addPositionalArguments("pos0") - .addPositionalArguments("pos1") - .addNamedArguments(NamedArgument.newBuilder().setName("option1").setValue("val1").build()) - .addNamedArguments(NamedArgument.newBuilder().setName("option2").setValue("val2").build()) - // .putFiles("-Ofile", "test_file.txt") - .build(); + public void testAnonymousWithOutPermission() { + Subject user = SecurityUtils.getSubject(); + CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true"); + user.login(AnonymousAuthenticationToken.getInstance()); + + // TODO: implement this test + + // Form form = new Form("call=anonymous_no_permission"); + // Representation entity = form.getWebRepresentation(); + // Request request = new Request(Method.POST, "../test", entity); + // request.setRootRef(new Reference("bla")); + // request.getAttributes().put("SRID", "asdf1234"); + // request.setDate(new Date()); + // request.setHostRef("bla"); + // resource.init(null, request, new Response(null)); + + // resource.handle(); + // assertEquals(Status.CLIENT_ERROR_FORBIDDEN, + // resource.getResponse().getStatus()); + } + + @Test + public void testAnonymousWithPermission() { + Subject user = SecurityUtils.getSubject(); + CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true"); + user.login(AnonymousAuthenticationToken.getInstance()); + + // TODO: implement this test + + // Form form = new Form("call=anonymous_ok"); + // Representation entity = form.getWebRepresentation(); + // Request request = new Request(Method.POST, "../test", entity); + // request.setRootRef(new Reference("bla")); + // request.getAttributes().put("SRID", "asdf1234"); + // request.setDate(new Date()); + // request.setHostRef("bla"); + // resource.init(null, request, new Response(null)); + + // resource.handle(); + // assertEquals(Status.SUCCESS_OK, resource.getResponse().getStatus()); + } + + @Test + public void testExecuteServerSideScriptRequestToCommandline() throws Message { + ExecuteServerSideScriptRequest request = + ExecuteServerSideScriptRequest.newBuilder() + .setScriptFilename("my_script.py") + .addPositionalArguments("pos0") + .addPositionalArguments("pos1") + .addNamedArguments( + NamedArgument.newBuilder().setName("option1").setValue("val1").build()) + .addNamedArguments( + NamedArgument.newBuilder().setName("option2").setValue("val2").build()) + // .putFiles("-Ofile", "test_file.txt") + .build(); ArrayList<String> commandline = ServerSideScriptingServiceImpl.request2CommandLine(request); - // assertEquals(commandline, - // "my_script.py --option1=val1 --option2=val2 --file=.upload_files/test_file.txt pos0 pos1"); - // } + + assertEquals(commandline.get(0), "my_script.py"); + assertEquals(commandline.get(1), "--option1=val1"); + assertEquals(commandline.get(2), "--option2=val2"); + assertEquals(commandline.get(3), "pos0"); + assertEquals(commandline.get(4), "pos1"); + } + + @Test + public void testCallScript() throws Message { + + // TODO: Implement this test + + // ServerSideScriptingServiceImpl service = new ServerSideScriptingServiceImpl(); + // ExecuteServerSideScriptRequest request = + // ExecuteServerSideScriptRequest.newBuilder() + // .setScriptFilename("my_script.py") + // .addPositionalArguments("pos0") + // .addPositionalArguments("pos1") + // .addNamedArguments( + // NamedArgument.newBuilder().setName("option1").setValue("val1").build()) + // .addNamedArguments( + // NamedArgument.newBuilder().setName("option2").setValue("val2").build()) + // .setTimeoutMs(1000) + // .build(); + + // ExecuteServerSideScriptResponse response = service.callScript(request); + + // assertEquals(response.getCall(), "my_script.py"); + // assertEquals( + // response.getResult(), + // ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_SUCCESS); + // assertEquals(response.getStdout(), ""); // Assuming the script does not produce any output + // assertEquals(response.getStderr(), ""); // Assuming the script does not produce any error output + // assertEquals(response.getReturnCode(), 0); // Assuming the script returns 0 + // assertEquals(response.getDurationMs() > 0, true); // Check if duration is greater than 0 + } } -- GitLab