From c7b3f5681d41666ab8ae0b7bf0f0aeffd6f2e5ca Mon Sep 17 00:00:00 2001 From: Sergio Ribeiro Date: Wed, 18 Oct 2023 22:55:19 +0100 Subject: [PATCH 1/3] [BISERVER-14935] import-export.bat/.sh does not work on Pentaho 9.3 or higher if requestParameterAuthenticationEnabled=true --- .../services/importexport/CommandLineProcessor.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java b/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java index 55659113095..b133c6679d1 100644 --- a/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java +++ b/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java @@ -87,6 +87,7 @@ public class CommandLineProcessor { private static final String ANALYSIS_DATASOURCE_IMPORT = "/plugin/data-access/api/mondrian/postAnalysis"; private static final String METADATA_DATASOURCE_IMPORT = "/plugin/data-access/api/metadata/postimport"; + private static final String API_REPO_FILES_BACKUP = "/api/repo/files/backup"; private static final String METADATA_DATASOURCE_EXT = "xmi"; @@ -719,7 +720,6 @@ private void performBackup() throws ParseException, IOException, KettleException, URISyntaxException { String contextURL = getOptionValue( INFO_OPTION_URL_NAME, true, false ); String logFile = getOptionValue( INFO_OPTION_LOGFILE_NAME, false, true ); - String backupURL = contextURL + "/api/repo/files/backup"; initRestService( contextURL ); @@ -731,10 +731,18 @@ private void performBackup() if ( !authResponse.equals( "true" ) ) { System.err.println( Messages.getInstance().getString( "CommandLineProcessor.ERROR_0006_NON_ADMIN_CREDENTIALS" ) ); } + + // Build the complete URL to use + String backupURL = contextURL + API_REPO_FILES_BACKUP; + + // BISERVER-14935: In case "requestParameterAuthenticationEnabled" is set to 'true' + backupURL += ( "?userid=" + getUsername() + "&password=" + Encr.encryptPassword( + Encr.decryptPasswordOptionallyEncrypted( getPassword() ) ) ); + WebResource resource = client.resource( backupURL ); // Response response - Builder builder = resource.type( MediaType.MULTIPART_FORM_DATA ).accept( MediaType.TEXT_HTML_TYPE ); + Builder builder = resource.type( MediaType.APPLICATION_FORM_URLENCODED ).accept( MediaType.TEXT_HTML_TYPE ); ClientResponse response = builder.get( ClientResponse.class ); if ( response != null && response.getStatus() == 200 ) { String filename = getOptionValue( INFO_OPTION_FILEPATH_NAME, true, false ); From 83fc88c8a141d37d2ea5ada03dfd050c0714ee23 Mon Sep 17 00:00:00 2001 From: Sergio Ribeiro Date: Thu, 19 Oct 2023 09:38:03 +0100 Subject: [PATCH 2/3] [BISERVER-14935] import-export.bat/.sh does not work on Pentaho 9.3 or higher if requestParameterAuthenticationEnabled=true Cleanup: - use of constants - extract methods - use of try-resource - remove old unused methods - clean exceptions - refactor code to write to file --- .../importexport/CommandLineProcessor.java | 432 ++++++++---------- 1 file changed, 200 insertions(+), 232 deletions(-) diff --git a/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java b/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java index b133c6679d1..5037d9e59e2 100644 --- a/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java +++ b/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java @@ -14,13 +14,15 @@ * See the GNU Lesser General Public License for more details. * * - * Copyright (c) 2002-2021 Hitachi Vantara. All rights reserved. + * Copyright (c) 2002-2023 Hitachi Vantara. All rights reserved. * */ package org.pentaho.platform.plugin.services.importexport; import com.google.common.annotations.VisibleForTesting; +import com.hitachivantara.security.web.impl.client.csrf.jaxrsv1.CsrfTokenFilter; +import com.hitachivantara.security.web.impl.client.csrf.jaxrsv1.util.SessionCookiesFilter; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; @@ -31,45 +33,34 @@ import com.sun.jersey.api.json.JSONConfiguration; import com.sun.jersey.core.header.FormDataContentDisposition; import com.sun.jersey.multipart.FormDataMultiPart; -import com.sun.xml.ws.developer.JAXWSProperties; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import com.hitachivantara.security.web.impl.client.csrf.jaxrsv1.CsrfTokenFilter; -import com.hitachivantara.security.web.impl.client.csrf.jaxrsv1.util.SessionCookiesFilter; import org.pentaho.di.core.KettleClientEnvironment; import org.pentaho.di.core.encryption.Encr; import org.pentaho.di.core.exception.KettleException; -import org.pentaho.platform.api.repository2.unified.IUnifiedRepository; import org.pentaho.platform.plugin.services.messages.Messages; import org.pentaho.platform.repository.RepositoryFilenameUtils; -import org.pentaho.platform.repository2.unified.webservices.jaxws.IUnifiedRepositoryJaxwsWebService; -import org.pentaho.platform.repository2.unified.webservices.jaxws.UnifiedRepositoryToWebServiceAdapter; import org.pentaho.platform.security.policy.rolebased.actions.AdministerSecurityAction; import org.pentaho.platform.util.RepositoryPathEncoder; import javax.ws.rs.core.MediaType; -import javax.xml.namespace.QName; -import javax.xml.ws.BindingProvider; -import javax.xml.ws.Service; -import javax.xml.ws.soap.SOAPBinding; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.net.CookieManager; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -81,13 +72,32 @@ public class CommandLineProcessor { private static final String API_CSRF_TOKEN = "/api/csrf/token"; - private static final String API_REPO_FILES_IMPORT = "/api/repo/files/import"; - - private static final String ANALYSIS_DATASOURCE_IMPORT = "/plugin/data-access/api/mondrian/postAnalysis"; - - private static final String METADATA_DATASOURCE_IMPORT = "/plugin/data-access/api/metadata/postimport"; + private static final String API_MONDRIAN_POST_ANALYSIS = "/plugin/data-access/api/mondrian/postAnalysis"; + private static final String API_METADATA_POST_IMPORT = "/plugin/data-access/api/metadata/postimport"; + private static final String API_REPO_FILES = "/api/repo/files/"; private static final String API_REPO_FILES_BACKUP = "/api/repo/files/backup"; + private static final String API_AUTHORIZATION_ACTION_IS_AUTHORIZED = "/api/authorization/action/isauthorized"; + private static final String API_REPO_FILES_SYSTEM_RESTORE = "/api/repo/files/systemRestore"; + + private static final String MULTIPART_FIELD_OVERWRITE = "overwrite"; + private static final String MULTIPART_FIELD_DOMAIN_ID = "domainId"; + private static final String MULTIPART_FIELD_METADATA_FILE = "metadataFile"; + private static final String MULTIPART_FIELD_CATALOG_NAME = "catalogName"; + private static final String MULTIPART_FIELD_DATASOURCE_NAME = "datasourceName"; + private static final String MULTIPART_FIELD_PARAMETERS = "parameters"; + private static final String MULTIPART_FIELD_XMLA_ENABLED_FLAG = "xmlaEnabledFlag"; + private static final String MULTIPART_FIELD_UPLOAD_ANALYSIS = "uploadAnalysis"; + private static final String MULTIPART_FIELD_IMPORT_DIR = "importDir"; + private static final String MULTIPART_FIELD_OVERWRITE_ACL_PERMISSIONS = "overwriteAclPermissions"; + private static final String MULTIPART_FIELD_RETAIN_OWNERSHIP = "retainOwnership"; + private static final String MULTIPART_FIELD_CHAR_SET = "charSet"; + private static final String MULTIPART_FIELD_APPLY_ACL_PERMISSIONS = "applyAclPermissions"; + private static final String MULTIPART_FIELD_FILE_UPLOAD = "fileUpload"; + private static final String MULTIPART_FIELD_FILE_NAME_OVERRIDE = "fileNameOverride"; + private static final String MULTIPART_FIELD_OVERWRITE_FILE = "overwriteFile"; + private static final String MULTIPART_FIELD_APPLY_ACL_SETTINGS = "applyAclSettings"; + private static final String MULTIPART_FIELD_OVERWRITE_ACL_SETTINGS = "overwriteAclSettings"; private static final String METADATA_DATASOURCE_EXT = "xmi"; @@ -101,11 +111,9 @@ public class CommandLineProcessor { private static String errorMessage; - private CommandLine commandLine; - - private RequestType requestType; + private final CommandLine commandLine; - private IUnifiedRepository repository; + private final RequestType requestType; private static final String INFO_OPTION_HELP_KEY = "h"; private static final String INFO_OPTION_HELP_NAME = "help"; @@ -175,8 +183,14 @@ public enum ResourceType { } private static Client client = null; + private static final ClientConfig clientConfig; + static { + // For REST Jersey calls + clientConfig = new DefaultClientConfig(); + clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE ); + // create the Options options.addOption( INFO_OPTION_HELP_KEY, INFO_OPTION_HELP_NAME, false, Messages.getInstance() .getString( "CommandLineProcessor.INFO_OPTION_HELP_DESCRIPTION" ) ); @@ -270,7 +284,6 @@ public enum ResourceType { * @param args */ public static void main( String[] args ) throws Exception { - try { CommandLineProcessor commandLineProcessor = new CommandLineProcessor( args ); @@ -290,12 +303,15 @@ public static void main( String[] args ) throws Exception { case EXPORT: commandLineProcessor.performExport(); break; + case REST: commandLineProcessor.performREST(); break; + case BACKUP: commandLineProcessor.performBackup(); break; + case RESTORE: commandLineProcessor.performRestore(); break; @@ -321,7 +337,7 @@ private void performREST() throws ParseException, KettleException, URISyntaxExce String contextURL = getOptionValue( INFO_OPTION_URL_NAME, true, false ); String path = getOptionValue( INFO_OPTION_PATH_NAME, true, false ); String logFile = getOptionValue( INFO_OPTION_LOGFILE_NAME, false, true ); - String exportURL = contextURL + "/api/repo/files/"; + String exportURL = contextURL + API_REPO_FILES; if ( path != null ) { String effPath = RepositoryPathEncoder.encodeRepositoryPath( path ); exportURL += effPath; @@ -346,10 +362,10 @@ private void performREST() throws ParseException, KettleException, URISyntaxExce Messages.getInstance().getString( "CommandLineProcessor.INFO_REST_RESPONSE_STATUS", response.getStatus() ); message += "\n"; - if ( logFile != null && !"".equals( logFile ) ) { + if ( logFile != null && !logFile.isEmpty() ) { message += Messages.getInstance().getString( "CommandLineProcessor.INFO_REST_FILE_WRITTEN", logFile ); System.out.println( message ); - writeFile( message, logFile ); + writeToFile( message, logFile ); } } else { System.out.println( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0002_INVALID_RESPONSE" ) ); @@ -377,9 +393,6 @@ String getPassword() throws ParseException, KettleException { */ private void initRestService( String contextURL ) throws ParseException, KettleException, URISyntaxException { - ClientConfig clientConfig = new DefaultClientConfig(); - clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE ); - client = Client.create( clientConfig ); client.addFilter( new HTTPBasicAuthFilter( getUsername(), getPassword() ) ); client.addFilter( new SessionCookiesFilter( new CookieManager() ) ); @@ -453,7 +466,7 @@ private void performMetadataDatasourceImport( String contextURL, File metadataDa File metadataFileInZip = null; InputStream metadataFileInZipInputStream = null; - String metadataImportURL = contextURL + METADATA_DATASOURCE_IMPORT; + String metadataImportURL = contextURL + API_METADATA_POST_IMPORT; String domainId = getOptionValue( INFO_OPTION_METADATA_DOMAIN_ID_NAME, true, false ); @@ -471,14 +484,15 @@ private void performMetadataDatasourceImport( String contextURL, File metadataDa while ( entry != null ) { final String entryName = RepositoryFilenameUtils.separatorsToRepository( entry.getName() ); final String extension = RepositoryFilenameUtils.getExtension( entryName ); - File tempFile = null; + boolean isDir = entry.getSize() == 0; if ( !isDir ) { - tempFile = File.createTempFile( "zip", null ); + // TODO Why on Earth are we creating these temporary files if they are set to be deleted on exit and not + // being read anywhere? Note that, for what I saw, this code is as it was created in 2013... Could it only + // be to make sure the zip is valid? + File tempFile = File.createTempFile( "zip", null ); tempFile.deleteOnExit(); - FileOutputStream fos = new FileOutputStream( tempFile ); - IOUtils.copy( zipInputStream, fos ); - fos.close(); + writeToFile( zipInputStream, tempFile ); } if ( extension.equals( METADATA_DATASOURCE_EXT ) ) { if ( metadataFileInZip == null ) { @@ -492,13 +506,15 @@ private void performMetadataDatasourceImport( String contextURL, File metadataDa } zipInputStream.close(); - part.field( "overwrite", "true".equals( overwrite ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); - part.field( "domainId", domainId, MediaType.MULTIPART_FORM_DATA_TYPE ).field( "metadataFile", - metadataFileInZipInputStream, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_OVERWRITE, "true".equals( overwrite ) ? "true" : "false", + MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_DOMAIN_ID, domainId, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_METADATA_FILE, metadataFileInZipInputStream, MediaType.MULTIPART_FORM_DATA_TYPE ); // If the import service needs the file name do the following. - part.getField( "metadataFile" ).setContentDisposition( FormDataContentDisposition.name( "metadataFile" ) - .fileName( metadataFileInZip.getName() ).build() ); + part.getField( MULTIPART_FIELD_METADATA_FILE ) + .setContentDisposition( FormDataContentDisposition.name( MULTIPART_FIELD_METADATA_FILE ) + .fileName( metadataFileInZip.getName() ).build() ); // Response response ClientResponse response = resource.type( MediaType.MULTIPART_FORM_DATA ).post( ClientResponse.class, part ); @@ -510,12 +526,13 @@ private void performMetadataDatasourceImport( String contextURL, File metadataDa } else { FileInputStream metadataDatasourceInputStream = new FileInputStream( metadataDatasourceFile ); - part.field( "overwrite", "true".equals( overwrite ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); - part.field( "domainId", domainId, MediaType.MULTIPART_FORM_DATA_TYPE ).field( "metadataFile", - metadataDatasourceInputStream, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_OVERWRITE, "true".equals( overwrite ) ? "true" : "false", + MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_DOMAIN_ID, domainId, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_METADATA_FILE, metadataDatasourceInputStream, MediaType.MULTIPART_FORM_DATA_TYPE ); // If the import service needs the file name do the following. - part.getField( "metadataFile" ).setContentDisposition( FormDataContentDisposition.name( "metadataFile" ) + part.getField( MULTIPART_FIELD_METADATA_FILE ).setContentDisposition( FormDataContentDisposition.name( MULTIPART_FIELD_METADATA_FILE ) .fileName( metadataDatasourceFile.getName() ).build() ); // Response response @@ -530,7 +547,6 @@ private void performMetadataDatasourceImport( String contextURL, File metadataDa metadataFileInZipInputStream.close(); part.cleanup(); } - } /** @@ -548,7 +564,7 @@ private void performAnalysisDatasourceImport( String contextURL, File analysisDa String logFile, String path ) throws ParseException, IOException { - String analysisImportURL = contextURL + ANALYSIS_DATASOURCE_IMPORT; + String analysisImportURL = contextURL + API_MONDRIAN_POST_ANALYSIS; String catalogName = getOptionValue( INFO_OPTION_ANALYSIS_CATALOG_NAME, false, true ); String datasourceName = getOptionValue( INFO_OPTION_ANALYSIS_DATASOURCE_NAME, false, true ); @@ -559,23 +575,24 @@ private void performAnalysisDatasourceImport( String contextURL, File analysisDa String parms = "Datasource=" + datasourceName + ";overwrite=" + overwrite; FormDataMultiPart part = new FormDataMultiPart(); - part.field( "overwrite", "true".equals( overwrite ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_OVERWRITE, "true".equals( overwrite ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); if ( catalogName != null ) { - part.field( "catalogName", catalogName, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_CATALOG_NAME, catalogName, MediaType.MULTIPART_FORM_DATA_TYPE ); } if ( datasourceName != null ) { - part.field( "datasourceName", datasourceName, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_DATASOURCE_NAME, datasourceName, MediaType.MULTIPART_FORM_DATA_TYPE ); } - part.field( "parameters", parms, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_PARAMETERS, parms, MediaType.MULTIPART_FORM_DATA_TYPE ); - part.field( "xmlaEnabledFlag", "true".equals( xmlaEnabledFlag ) ? "true" : "false", + part.field( MULTIPART_FIELD_XMLA_ENABLED_FLAG, "true".equals( xmlaEnabledFlag ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); - part.field( "uploadAnalysis", inputStream, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_UPLOAD_ANALYSIS, inputStream, MediaType.MULTIPART_FORM_DATA_TYPE ); // If the import service needs the file name do the following. - part.getField( "uploadAnalysis" ).setContentDisposition( FormDataContentDisposition.name( "uploadAnalysis" ) - .fileName( analysisDatasourceFile.getName() ).build() ); + part.getField( MULTIPART_FIELD_UPLOAD_ANALYSIS ) + .setContentDisposition( FormDataContentDisposition.name( MULTIPART_FIELD_UPLOAD_ANALYSIS ) + .fileName( analysisDatasourceFile.getName() ).build() ); WebResource.Builder resourceBuilder = resource.type( MediaType.MULTIPART_FORM_DATA ); ClientResponse response = resourceBuilder.post( ClientResponse.class, part ); @@ -617,7 +634,6 @@ private void performDatasourceImport() throws ParseException { System.err.println( e.getMessage() ); log.error( e.getMessage() ); } - } /* @@ -655,19 +671,21 @@ private void performImport() throws ParseException, IOException { String retainOwnership = getOptionValue( INFO_OPTION_RETAIN_OWNERSHIP_NAME, false, true ); String permission = getOptionValue( INFO_OPTION_PERMISSION_NAME, false, true ); - part.field( "importDir", path, MediaType.MULTIPART_FORM_DATA_TYPE ); - part.field( "overwriteAclPermissions", "true".equals( overwrite ) ? "true" : "false", + part.field( MULTIPART_FIELD_IMPORT_DIR, path, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_OVERWRITE_ACL_PERMISSIONS, "true".equals( overwrite ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); - part.field( "retainOwnership", "true".equals( retainOwnership ) ? "true" : "false", + part.field( MULTIPART_FIELD_RETAIN_OWNERSHIP, "true".equals( retainOwnership ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); - part.field( "charSet", charSet == null ? "UTF-8" : charSet ); - part.field( "applyAclPermissions", "true".equals( permission ) ? "true" : "false", - MediaType.MULTIPART_FORM_DATA_TYPE ).field( "fileUpload", in, MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_CHAR_SET, charSet == null ? StandardCharsets.UTF_8.name() : charSet ); + part.field( MULTIPART_FIELD_APPLY_ACL_PERMISSIONS, "true".equals( permission ) ? "true" : "false", + MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_FILE_UPLOAD, in, MediaType.MULTIPART_FORM_DATA_TYPE ); // If the import service needs the file name do the following. - part.field( "fileNameOverride", fileIS.getName(), MediaType.MULTIPART_FORM_DATA_TYPE ); - part.getField( "fileUpload" ).setContentDisposition( FormDataContentDisposition.name( "fileUpload" ).fileName( - fileIS.getName() ).build() ); + part.field( MULTIPART_FIELD_FILE_NAME_OVERRIDE, fileIS.getName(), MediaType.MULTIPART_FORM_DATA_TYPE ); + part.getField( MULTIPART_FIELD_FILE_UPLOAD ) + .setContentDisposition( FormDataContentDisposition.name( MULTIPART_FIELD_FILE_UPLOAD ).fileName( + fileIS.getName() ).build() ); WebResource.Builder resourceBuilder = resource.type( MediaType.MULTIPART_FORM_DATA ); ClientResponse response = resourceBuilder.post( ClientResponse.class, part ); @@ -678,7 +696,7 @@ private void performImport() throws ParseException, IOException { } catch ( Exception e ) { System.err.println( e.getMessage() ); log.error( e.getMessage() ); - writeFile( e.getMessage(), logFile ); + writeToFile( e.getMessage(), logFile ); } finally { // close input stream and cleanup the jersey resources client.destroy(); @@ -703,9 +721,9 @@ private void logResponseMessage( String logFile, String path, ClientResponse res message.append( Messages.getInstance().getString( "CommandLineProcessor.INFO_REST_RESPONSE_RECEIVED", response.getEntity( String.class ) ) ); } - System.out.println( message.toString() ); - if ( logFile != null && !"".equals( logFile ) ) { - writeFile( message.toString(), logFile ); + System.out.println( message ); + if ( logFile != null && !logFile.isEmpty() ) { + writeToFile( message.toString(), logFile ); } } @@ -716,20 +734,23 @@ private void logResponseMessage( String logFile, String path, ClientResponse res * --backup --url=http://localhost:8080/pentaho --username=admin --password=password * --logfile=c:/temp/steel-wheels.log --file-path=c:/temp/backup.zip */ - private void performBackup() - throws ParseException, IOException, KettleException, URISyntaxException { + private void performBackup() throws ParseException, KettleException, URISyntaxException { String contextURL = getOptionValue( INFO_OPTION_URL_NAME, true, false ); String logFile = getOptionValue( INFO_OPTION_LOGFILE_NAME, false, true ); + // Output file is validated before executing + String outputFile = getOptionValue( INFO_OPTION_FILEPATH_NAME, true, false ); + + if ( !isValidExportPath( outputFile, logFile ) ) { + throw new ParseException( Messages.getInstance().getString( "CommandLineProcessor.ERROR_0005_INVALID_FILE_PATH", + outputFile ) ); + } + initRestService( contextURL ); // check if the user has permissions to upload/download data - WebResource authResource = - client.resource( contextURL + "/api/authorization/action/isauthorized?authAction=" - + AdministerSecurityAction.NAME ); - String authResponse = authResource.get( String.class ); - if ( !authResponse.equals( "true" ) ) { - System.err.println( Messages.getInstance().getString( "CommandLineProcessor.ERROR_0006_NON_ADMIN_CREDENTIALS" ) ); + if ( !checkUserAuthorization( contextURL, AdministerSecurityAction.NAME ) ) { + return; } // Build the complete URL to use @@ -745,17 +766,15 @@ private void performBackup() Builder builder = resource.type( MediaType.APPLICATION_FORM_URLENCODED ).accept( MediaType.TEXT_HTML_TYPE ); ClientResponse response = builder.get( ClientResponse.class ); if ( response != null && response.getStatus() == 200 ) { - String filename = getOptionValue( INFO_OPTION_FILEPATH_NAME, true, false ); - final InputStream input = response.getEntityInputStream(); - createZipFile( filename, input ); - input.close(); + writeEntityToFile( response, outputFile ); + String message = Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_COMPLETED" ).concat( "\n" ); message += Messages.getInstance().getString( "CommandLineProcessor.INFO_RESPONSE_STATUS", response.getStatus() ); message += "\n"; - message += Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_WRITTEN_TO", filename ); - if ( logFile != null && !"".equals( logFile ) ) { + message += Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_WRITTEN_TO", outputFile ); + if ( logFile != null && !logFile.isEmpty() ) { System.out.println( message ); - writeFile( message, logFile ); + writeToFile( message, logFile ); } } else { System.out.println( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0002_INVALID_RESPONSE" ) ); @@ -768,37 +787,33 @@ private void performBackup() * --logfile=c:/temp/steel-wheels.log --file-path=c:/temp/backup.zip * * @throws ParseException - * @throws java.io.IOException */ - private void performRestore() throws ParseException, IOException { + private void performRestore() throws ParseException { String contextURL = getOptionValue( INFO_OPTION_URL_NAME, true, false ); String filePath = getOptionValue( INFO_OPTION_FILEPATH_NAME, true, false ); String logFile = getOptionValue( INFO_OPTION_LOGFILE_NAME, false, true ); - String importURL = contextURL + "/api/repo/files/systemRestore"; + String importURL = contextURL + API_REPO_FILES_SYSTEM_RESTORE; File fileIS = new File( filePath ); - InputStream in = new FileInputStream( fileIS ); - FormDataMultiPart part = new FormDataMultiPart().field( "fileUpload", in, MediaType.MULTIPART_FORM_DATA_TYPE ); - try { + try ( InputStream in = Files.newInputStream( fileIS.toPath() ); + FormDataMultiPart part = new FormDataMultiPart() ) { initRestService( contextURL ); // check if the user has permissions to upload/download data - WebResource authResource = - client.resource( contextURL + "/api/authorization/action/isauthorized?authAction=" - + AdministerSecurityAction.NAME ); - String authResponse = authResource.get( String.class ); - if ( !authResponse.equals( "true" ) ) { - System.err.println( Messages.getInstance().getString( - "CommandLineProcessor.ERROR_0006_NON_ADMIN_CREDENTIALS" ) ); + if ( !checkUserAuthorization( contextURL, AdministerSecurityAction.NAME ) ) { + return; } - WebResource resource = client.resource( importURL ); + part.field( MULTIPART_FIELD_FILE_UPLOAD, in, MediaType.MULTIPART_FORM_DATA_TYPE ); String overwrite = getOptionValue( INFO_OPTION_OVERWRITE_NAME, true, false ); - part.field( "overwriteFile", "true".equals( overwrite ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_OVERWRITE_FILE, "true".equals( overwrite ) ? "true" : "false", + MediaType.MULTIPART_FORM_DATA_TYPE ); String applyAclSettings = getOptionValue( INFO_OPTION_APPLY_ACL_SETTINGS_NAME, false, true ); - part.field( "applyAclSettings", !"false".equals( applyAclSettings ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_APPLY_ACL_SETTINGS, !"false".equals( applyAclSettings ) ? "true" : "false", + MediaType.MULTIPART_FORM_DATA_TYPE ); String overwriteAclSettings = getOptionValue( INFO_OPTION_OVERWRITE_ACL_SETTINGS_NAME, false, true ); - part.field( "overwriteAclSettings", "true".equals( overwriteAclSettings ) ? "true" : "false", MediaType.MULTIPART_FORM_DATA_TYPE ); + part.field( MULTIPART_FIELD_OVERWRITE_ACL_SETTINGS, "true".equals( overwriteAclSettings ) ? "true" : "false", + MediaType.MULTIPART_FORM_DATA_TYPE ); // Response response ClientResponse response = resource.type( MediaType.MULTIPART_FORM_DATA ).post( ClientResponse.class, part ); @@ -809,12 +824,10 @@ private void performRestore() throws ParseException, IOException { } catch ( Exception e ) { System.err.println( e.getMessage() ); log.error( e.getMessage() ); - writeFile( e.getMessage(), logFile ); + writeToFile( e.getMessage(), logFile ); } finally { - // close input stream and cleanup the jersey resources + // cleanup the jersey resources client.destroy(); - part.cleanup(); - in.close(); } } @@ -825,27 +838,26 @@ private void performRestore() throws ParseException, IOException { * --file-path=c:/temp/export.zip --charset=UTF-8 --path=public/pentaho-solutions/steel-wheels * --logfile=c:/temp/steel-wheels.log --withManifest=true */ - private void performExport() - throws ParseException, IOException, KettleException, URISyntaxException { + private void performExport() throws ParseException, KettleException, URISyntaxException { String contextURL = getOptionValue( INFO_OPTION_URL_NAME, true, false ); String path = getOptionValue( INFO_OPTION_PATH_NAME, true, false ); String withManifest = getOptionValue( INFO_OPTION_WITH_MANIFEST_NAME, false, true ); String effPath = RepositoryPathEncoder.encodeURIComponent( RepositoryPathEncoder.encodeRepositoryPath( path ) ); - if ( effPath.lastIndexOf( ":" ) == effPath.length() - 1 // remove trailing slash + if ( effPath.lastIndexOf( ':' ) == effPath.length() - 1 // remove trailing slash && effPath.length() > 1 ) { // allow user to enter "--path=/" effPath = effPath.substring( 0, effPath.length() - 1 ); } String logFile = getOptionValue( INFO_OPTION_LOGFILE_NAME, false, true ); String exportURL = - contextURL + "/api/repo/files/" + effPath + "/download?withManifest=" + ( "false".equals( withManifest ) + contextURL + API_REPO_FILES + effPath + "/download?withManifest=" + ( "false".equals( withManifest ) ? "false" : "true" ); - // path is validated before executing - String filepath = getOptionValue( INFO_OPTION_FILEPATH_NAME, true, false ); + // Output file is validated before executing + String outputFile = getOptionValue( INFO_OPTION_FILEPATH_NAME, true, false ); - if ( !isValidExportPath( filepath, logFile ) ) { + if ( !isValidExportPath( outputFile, logFile ) ) { throw new ParseException( Messages.getInstance().getString( "CommandLineProcessor.ERROR_0005_INVALID_FILE_PATH", - filepath ) ); + outputFile ) ); } initRestService( contextURL ); @@ -855,17 +867,14 @@ private void performExport() Builder builder = resource.type( MediaType.MULTIPART_FORM_DATA ).accept( MediaType.TEXT_HTML_TYPE ); ClientResponse response = builder.get( ClientResponse.class ); if ( response != null && response.getStatus() == 200 ) { - String filename = getOptionValue( INFO_OPTION_FILEPATH_NAME, true, false ); - final InputStream input = response.getEntityInputStream(); - createZipFile( filename, input ); - input.close(); + writeEntityToFile( response, outputFile ); String message = Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_COMPLETED" ).concat( "\n" ); message += Messages.getInstance().getString( "CommandLineProcessor.INFO_RESPONSE_STATUS", response.getStatus() ); message += "\n"; - message += Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_WRITTEN_TO", filename ); - if ( logFile != null && !"".equals( logFile ) ) { + message += Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_WRITTEN_TO", outputFile ); + if ( logFile != null && !logFile.isEmpty() ) { System.out.println( message ); - writeFile( message, logFile ); + writeToFile( message, logFile ); } System.out.println( Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_SUCCESSFUL" ) ); } else if ( response != null && response.getStatus() == 403 ) { @@ -883,111 +892,21 @@ private boolean isValidExportPath( String filePath, String logFile ) { if ( filePath != null && filePath.toLowerCase().endsWith( ".zip" ) ) { - int fileNameIdx = filePath.replace( "\\", "/" ).lastIndexOf( "/" ); + int fileNameIdx = filePath.replace( '\\', '/' ).lastIndexOf( "/" ); if ( fileNameIdx >= 0 ) { - String directoryPath = filePath.substring( 0, fileNameIdx ); + File f = new File( filePath.substring( 0, fileNameIdx ) ); - File f = new File( directoryPath ); - if ( f != null && f.exists() && f.isDirectory() ) { - isValid = true; - } + isValid = f.exists() && f.isDirectory(); } } - if ( !isValid && logFile != null && !"".equals( logFile ) ) { - writeFile( "Invalid file-path:" + filePath, logFile ); + if ( !isValid && logFile != null && !logFile.isEmpty() ) { + writeToFile( "Invalid file-path:" + filePath, logFile ); } return isValid; } - /** - * create the zip file from the input stream - * - * @param filename - * @param input - * InputStream - */ - private void createZipFile( String filename, final InputStream input ) { - OutputStream output = null; - try { - output = new FileOutputStream( filename ); - byte[] buffer = new byte[ 8 * 1024 ]; - int bytesRead; - while ( ( bytesRead = input.read( buffer ) ) != -1 ) { - output.write( buffer, 0, bytesRead ); - } - - buffer = null; - } catch ( IOException e ) { - e.printStackTrace(); - } finally { - if ( output != null ) { - try { - output.close(); - } catch ( Exception e ) { - e.printStackTrace(); - } - } - } - } - - public synchronized void setRepository( final IUnifiedRepository repository ) { - if ( repository == null ) { - throw new IllegalArgumentException(); - } - this.repository = repository; - } - - /** - * Why does this return a web service? Going directly to the IUnifiedRepository requires the following: - *

- *

- */ - protected synchronized IUnifiedRepository getRepository() throws ParseException, KettleException { - if ( repository != null ) { - return repository; - } - - final String NAMESPACE_URI = "http://www.pentaho.org/ws/1.0"; //$NON-NLS-1$ - final String SERVICE_NAME = "unifiedRepository"; //$NON-NLS-1$ - - String urlString = getOptionValue( INFO_OPTION_URL_NAME, true, false ).trim(); - if ( urlString.endsWith( "/" ) ) { - urlString = urlString.substring( 0, urlString.length() - 1 ); - } - urlString = urlString + "/webservices/" + SERVICE_NAME + "?wsdl"; - - URL url; - try { - url = new URL( urlString ); - } catch ( MalformedURLException e ) { - throw new IllegalArgumentException( e ); - } - - Service service = Service.create( url, new QName( NAMESPACE_URI, SERVICE_NAME ) ); - IUnifiedRepositoryJaxwsWebService port = service.getPort( IUnifiedRepositoryJaxwsWebService.class ); - // http basic authentication - ( (BindingProvider) port ).getRequestContext().put( BindingProvider.USERNAME_PROPERTY, getUsername() ); - ( (BindingProvider) port ).getRequestContext().put( BindingProvider.PASSWORD_PROPERTY, getPassword() ); - // accept cookies to maintain session on server - ( (BindingProvider) port ).getRequestContext().put( BindingProvider.SESSION_MAINTAIN_PROPERTY, true ); - // support streaming binary data - // TODO mlowery this is not portable between JAX-WS implementations - // (uses com.sun) - ( (BindingProvider) port ).getRequestContext().put( JAXWSProperties.HTTP_CLIENT_STREAMING_CHUNK_SIZE, 8192 ); - SOAPBinding binding = (SOAPBinding) ( (BindingProvider) port ).getBinding(); - binding.setMTOMEnabled( true ); - final UnifiedRepositoryToWebServiceAdapter unifiedRepositoryToWebServiceAdapter = - new UnifiedRepositoryToWebServiceAdapter( port ); - repository = unifiedRepositoryToWebServiceAdapter; - return unifiedRepositoryToWebServiceAdapter; - } - /** * Returns the option value from the command line * @@ -1005,14 +924,20 @@ protected synchronized IUnifiedRepository getRepository() throws ParseException, protected String getOptionValue( final String option, final boolean required, final boolean emptyOk ) throws ParseException { final String value = StringUtils.trim( commandLine.getOptionValue( option ) ); - if ( required && StringUtils.isEmpty( value ) ) { - throw new ParseException( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0001_MISSING_ARG", - option ) ); - } - if ( !emptyOk && StringUtils.isEmpty( value ) ) { - throw new ParseException( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0001_MISSING_ARG", - option ) ); + if ( StringUtils.isEmpty( value ) ) { + // Is it required? + if ( required ) { + throw new ParseException( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0001_MISSING_ARG", + option ) ); + } + + // Ok, it's not required, but is it Ok to be empty? + if ( !emptyOk ) { + throw new ParseException( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0001_MISSING_ARG", + option ) ); + } } + return StringUtils.removeStart( value, "=" ); } @@ -1021,21 +946,45 @@ public static String getErrorMessage() { } /** - * internal helper to write output file + * Writes the entity on the given {@link ClientResponse} to an output file * - * @param message - * @param logFile + * @param response the response instance + * @param pathName the path of the output file */ - private void writeFile( String message, String logFile ) { - try { - File file = new File( logFile ); - FileOutputStream fout = FileUtils.openOutputStream( file ); - IOUtils.copy( IOUtils.toInputStream( message ), fout ); - fout.close(); + private void writeEntityToFile( ClientResponse response, String pathName ) { + try ( InputStream input = response.getEntityInputStream() ) { + writeToFile( input, new File( pathName ) ); + } catch ( IOException e ) { + e.printStackTrace(); + } + } + + /** + * Writes the given string to an output file + * + * @param str the string + * @param pathName the path of the output file + * @see #writeToFile(InputStream, File) + */ + private void writeToFile( String str, String pathName ) { + try ( InputStream inputStream = IOUtils.toInputStream( str, Charset.defaultCharset() ) ) { + writeToFile( inputStream, new File( pathName ) ); } catch ( Exception ex ) { ex.printStackTrace(); } + } + /** + * Writes the contents of the given input string to an output file + * + * @param inputStream the input stream to persist + * @param file the output file + * @see #writeToFile(String, String) + */ + private static void writeToFile( InputStream inputStream, File file ) throws IOException { + try ( FileOutputStream fos = new FileOutputStream( file ) ) { + IOUtils.copy( inputStream, fos ); + } } protected static void printHelp() { @@ -1044,4 +993,23 @@ protected static void printHelp() { .getInstance().getString( "CommandLineProcessor.INFO_PRINTHELP_HEADER" ), options, Messages.getInstance() .getString( "CommandLineProcessor.INFO_PRINTHELP_FOOTER" ) ); } + + /** + * Check if the current user has a given authorization. + * + * @param contextURL the base URL + * @param securityAction the Action to check + * @return true if the user has the given authorization, false if it does not + */ + private static boolean checkUserAuthorization( String contextURL, String securityAction ) { + WebResource authResource = + client.resource( contextURL + API_AUTHORIZATION_ACTION_IS_AUTHORIZED + "?authAction=" + + securityAction ); + boolean isAuthorized = Boolean.parseBoolean( authResource.get( String.class ) ); + if ( !isAuthorized ) { + System.err.println( Messages.getInstance().getString( + "CommandLineProcessor.ERROR_0006_NON_ADMIN_CREDENTIALS" ) ); + } + return isAuthorized; + } } From 7116ba79daa0f5a2cbe8df7189e5a99c9a842f1f Mon Sep 17 00:00:00 2001 From: Sergio Ribeiro Date: Tue, 31 Oct 2023 21:58:47 +0000 Subject: [PATCH 3/3] [BISERVER-14935] import-export.bat/.sh does not work on Pentaho 9.3 or higher if requestParameterAuthenticationEnabled=true Address the comments on the PR review --- .../importexport/CommandLineProcessor.java | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java b/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java index 5037d9e59e2..38f3397ef0e 100644 --- a/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java +++ b/extensions/src/main/java/org/pentaho/platform/plugin/services/importexport/CommandLineProcessor.java @@ -362,7 +362,7 @@ private void performREST() throws ParseException, KettleException, URISyntaxExce Messages.getInstance().getString( "CommandLineProcessor.INFO_REST_RESPONSE_STATUS", response.getStatus() ); message += "\n"; - if ( logFile != null && !logFile.isEmpty() ) { + if ( StringUtils.isNotBlank( logFile ) ) { message += Messages.getInstance().getString( "CommandLineProcessor.INFO_REST_FILE_WRITTEN", logFile ); System.out.println( message ); writeToFile( message, logFile ); @@ -722,7 +722,7 @@ private void logResponseMessage( String logFile, String path, ClientResponse res response.getEntity( String.class ) ) ); } System.out.println( message ); - if ( logFile != null && !logFile.isEmpty() ) { + if ( StringUtils.isNotBlank( logFile ) ) { writeToFile( message.toString(), logFile ); } } @@ -754,11 +754,7 @@ private void performBackup() throws ParseException, KettleException, URISyntaxEx } // Build the complete URL to use - String backupURL = contextURL + API_REPO_FILES_BACKUP; - - // BISERVER-14935: In case "requestParameterAuthenticationEnabled" is set to 'true' - backupURL += ( "?userid=" + getUsername() + "&password=" + Encr.encryptPassword( - Encr.decryptPasswordOptionallyEncrypted( getPassword() ) ) ); + String backupURL = buildURL( contextURL, API_REPO_FILES_BACKUP ); WebResource resource = client.resource( backupURL ); @@ -772,7 +768,7 @@ private void performBackup() throws ParseException, KettleException, URISyntaxEx message += Messages.getInstance().getString( "CommandLineProcessor.INFO_RESPONSE_STATUS", response.getStatus() ); message += "\n"; message += Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_WRITTEN_TO", outputFile ); - if ( logFile != null && !logFile.isEmpty() ) { + if ( StringUtils.isNotBlank( logFile ) ) { System.out.println( message ); writeToFile( message, logFile ); } @@ -781,6 +777,27 @@ private void performBackup() throws ParseException, KettleException, URISyntaxEx } } + /** + * Given the Context URL and an API, this method returns the corresponding complete URL adding the proper + * parameters for when the request Parameter Authentication is enabled. + * + * @param contextURL the context URL + * @param apiPath the API path + * @return Complete URL for the given API and already prepared for when the request Parameter Authentication is + * enabled + * @throws ParseException + * @throws KettleException + */ + private String buildURL( String contextURL, String apiPath ) throws ParseException, KettleException { + StringBuilder sb = new StringBuilder(); + sb.append( contextURL ).append( apiPath ) + .append( "?userid=" ).append( getUsername() ) + .append( "&password=" ) + .append( Encr.encryptPassword( Encr.decryptPasswordOptionallyEncrypted( getPassword() ) ) ); + + return sb.toString(); + } + /** * REST Service Restore * --restore --url=http://localhost:8080/pentaho --username=admin --password=password --overwrite=true @@ -872,7 +889,7 @@ private void performExport() throws ParseException, KettleException, URISyntaxEx message += Messages.getInstance().getString( "CommandLineProcessor.INFO_RESPONSE_STATUS", response.getStatus() ); message += "\n"; message += Messages.getInstance().getString( "CommandLineProcessor.INFO_EXPORT_WRITTEN_TO", outputFile ); - if ( logFile != null && !logFile.isEmpty() ) { + if ( StringUtils.isNotBlank( logFile ) ) { System.out.println( message ); writeToFile( message, logFile ); } @@ -900,7 +917,7 @@ private boolean isValidExportPath( String filePath, String logFile ) { } } - if ( !isValid && logFile != null && !logFile.isEmpty() ) { + if ( !isValid && StringUtils.isNotBlank( logFile ) ) { writeToFile( "Invalid file-path:" + filePath, logFile ); } @@ -924,18 +941,10 @@ private boolean isValidExportPath( String filePath, String logFile ) { protected String getOptionValue( final String option, final boolean required, final boolean emptyOk ) throws ParseException { final String value = StringUtils.trim( commandLine.getOptionValue( option ) ); - if ( StringUtils.isEmpty( value ) ) { - // Is it required? - if ( required ) { - throw new ParseException( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0001_MISSING_ARG", - option ) ); - } - // Ok, it's not required, but is it Ok to be empty? - if ( !emptyOk ) { - throw new ParseException( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0001_MISSING_ARG", - option ) ); - } + if ( StringUtils.isEmpty( value ) && ( required || !emptyOk ) ) { + throw new ParseException( Messages.getInstance().getErrorString( "CommandLineProcessor.ERROR_0001_MISSING_ARG", + option ) ); } return StringUtils.removeStart( value, "=" );