/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2015 Pentaho Corporation. All rights reserved. */ package org.pentaho.marketplace.domain.services; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.karaf.features.FeaturesService; import org.apache.karaf.kar.KarService; import org.osgi.framework.Bundle; import org.osgi.service.cm.ConfigurationAdmin; import org.pentaho.di.core.Result; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleXMLException; import org.pentaho.di.core.logging.LogLevel; import org.pentaho.di.core.parameters.UnknownParamException; import org.pentaho.di.job.Job; import org.pentaho.di.job.JobMeta; import org.pentaho.marketplace.domain.model.entities.MarketEntryType; import org.pentaho.marketplace.domain.model.entities.interfaces.IPlugin; import org.pentaho.marketplace.domain.model.entities.interfaces.IPluginVersion; import org.pentaho.marketplace.domain.model.entities.serialization.IMarketplaceXmlSerializer; import org.pentaho.marketplace.domain.model.factories.interfaces.IDomainStatusMessageFactory; import org.pentaho.marketplace.domain.model.factories.interfaces.IPluginVersionFactory; import org.pentaho.marketplace.domain.model.factories.interfaces.IVersionDataFactory; import org.pentaho.marketplace.domain.services.helpers.Util; import org.pentaho.marketplace.domain.services.interfaces.IRemotePluginProvider; import org.pentaho.platform.api.engine.IApplicationContext; import org.pentaho.platform.api.engine.IPentahoSession; import org.pentaho.platform.api.engine.IPluginManager; import org.pentaho.platform.api.engine.ISecurityHelper; import org.pentaho.platform.engine.core.system.PentahoSessionHolder; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.util.VersionHelper; import org.pentaho.platform.util.VersionInfo; import org.pentaho.telemetry.ITelemetryService; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.xml.sax.InputSource; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Map; /** * Plugin service implementation for the BA server */ public class BaPluginService extends BasePluginService { //region Constants private static final String CLOSE_METHOD_NAME = "close"; private static final String PROPERTY_COLLECTION_SEPARATOR = ","; private static final String INSTALL_JOB_NAME = "download_and_install_plugin.kjb"; private static final String UNINSTALL_JOB_NAME = "uninstall_plugin.kjb"; private static final String CACHE_FOLDER = "system/plugin-cache/"; private static final String DOWNLOAD_CACHE_FOLDER = CACHE_FOLDER + "downloads/"; private static final String BACKUP_CACHE_FOLDER = CACHE_FOLDER + "backups/"; private static final String STAGING_CACHE_FOLDER = CACHE_FOLDER + "staging/"; private static final String MARKETPLACE_FOLDER = "system/marketplace"; private static final String SYSTEM_FOLDER = "system/"; private static final String PLUGIN_XML_FILE = "plugin.xml"; //endregion //region Properties public IMarketplaceXmlSerializer getXmlSerializer() { return this.xmlPluginsSerializer; } protected BaPluginService setXmlSerializer( IMarketplaceXmlSerializer serializer ) { this.xmlPluginsSerializer = serializer; return this; } private IMarketplaceXmlSerializer xmlPluginsSerializer; public ISecurityHelper getSecurityHelper() { return this.securityHelper; } protected BaPluginService setSecurityHelper( ISecurityHelper securityHelper ) { this.securityHelper = securityHelper; return this; } private ISecurityHelper securityHelper; @Override protected String getServerVersion() { if ( super.getServerVersion() == null ) { VersionInfo versionInfo = VersionHelper.getVersionInfo( PentahoSystem.class ); this.setServerVersion( versionInfo.getVersionNumber() ); } return super.getServerVersion(); } // TODO: see if there is a better way to encapsulate this public IPluginManager getPluginManager( IPentahoSession session ) { return PentahoSystem.get( IPluginManager.class, session ); } // TODO: see if there is a better way to encapsulate this protected IApplicationContext getApplicationContext() { if ( this.applicationContext == null ) { return PentahoSystem.getApplicationContext(); } return this.applicationContext; } protected BaPluginService setApplicationContext( IApplicationContext applicationContext ) { this.applicationContext = applicationContext; return this; } private IApplicationContext applicationContext; // TODO: see if there is a better way to encapsulate this. // Probably just pass in the session in the methods that require it. protected IPentahoSession getCurrentSession() { return PentahoSessionHolder.getSession(); } protected BaPluginService setCurrentSession( IPentahoSession session ) { PentahoSessionHolder.setSession( session ); return this; } /** * Gets the Roles which are authorized to install / unintall plugins * @return */ public Collection<String> getAuthorizedRoles() { return this.authorizedRoles; } /** * Sets the Roles which are authorized to install / unintall plugins * @return */ public void setAuthorizedRoles( Collection<String> authorizedRoles ) { if ( authorizedRoles == null ) { authorizedRoles = Collections.emptyList(); } this.authorizedRoles = authorizedRoles; } private Collection<String> authorizedRoles = Collections.emptyList(); /** * Sets roles which authorized to install / uninstall plugins from a string of comma separated values. * @param authorizedRolesString Comma separated string of roles ( e.g.: "roleA, roleB, roleC" ) */ public void setAuthorizedRoles( String authorizedRolesString ) { this.setAuthorizedRoles( this.parseStringCollection( authorizedRolesString, PROPERTY_COLLECTION_SEPARATOR ) ); } /** * Gets the user names which are authorized to install / unintall plugins * @return */ public Collection<String> getAuthorizedUsernames() { return authorizedUsernames; } /** * Sets the user names which are authorized to install / unintall plugins * @return */ public void setAuthorizedUsernames( Collection<String> authorizedUsernames ) { if ( authorizedUsernames == null ) { authorizedUsernames = Collections.emptyList(); } this.authorizedUsernames = authorizedUsernames; } private Collection<String> authorizedUsernames = Collections.emptyList(); /** * Sets the user names which authorized to install / uninstall plugins from a string of comma separated values. * @param authorizedUsernamesString Comma separated string of user names ( e.g.: "Jack, Lilly, Joe" ) */ public void setAuthorizedUsernames( String authorizedUsernamesString ) { this.setAuthorizedUsernames( this.parseStringCollection( authorizedUsernamesString, PROPERTY_COLLECTION_SEPARATOR ) ); } /** * Gets the OSGI bundle this service belongs to * @return */ public Bundle getBundle() { return this.bundle; } /** * Sets the OSGI bundle this service belongs to * @return */ public void setBundle( Bundle bundle ) { this.bundle = bundle; } private Bundle bundle; /** * Gets the relative path of the folder where the marketplace kettle transformations / jobs are stored. This path is * relative to the bundle base folder supplied by {@link Bundle#getLocation()} * * @return */ public String getRelativeKettleExecutionFolderPath() { return relativeKettleExecutionFolderPath; } /** * Sets the relative path of the folder where the marketplace kettle transformations / jobs are stored This path is * relative to the bundle base folder supplied by {@link Bundle#getLocation()} * * @param folderPath */ public void setRelativeKettleExecutionFolderPath( String folderPath ) { this.relativeKettleExecutionFolderPath = folderPath; } private String relativeKettleExecutionFolderPath; /** * Gets the absolute path to the folder where the marketplace transformations / jobs are stored and executed.* * @return */ public Path getAbsoluteKettleExecutionFolderPath() { return this.getMarketplaceFolder() .resolve( this.getRelativeKettleExecutionFolderPath() ); } /** * Gets the path to where the kettle files are within the Bundle. This path is relative to the bundle root. * @return */ public String getAbsoluteKettleResourcesSourcePath() { return this.absoluteKettleResourcesSourcePath; } public void setAbsoluteKettleResourcesSourcePath( String path ) { this.absoluteKettleResourcesSourcePath = path; } private String absoluteKettleResourcesSourcePath; protected Path getMarketplaceFolder() { String marketplacePath = this.getApplicationContext().getSolutionPath( MARKETPLACE_FOLDER ); return Paths.get( marketplacePath ).toAbsolutePath(); } private JobMeta getInstallJobMeta() { return this.getJobMeta( INSTALL_JOB_NAME ); } private JobMeta getUninstallJobMeta() { return this.getJobMeta( UNINSTALL_JOB_NAME ); } private JobMeta getJobMeta( String jobFileName ) { String jobFilePath = this.getAbsoluteKettleExecutionFolderPath().resolve( jobFileName ).toString(); JobMeta meta = null; try { meta = new JobMeta( jobFilePath, null ); } catch ( KettleXMLException e ) { this.getLogger().error( "Unable to create job meta from file path " + jobFilePath, e ); } return meta; } //endregion //region Constructors public BaPluginService( IRemotePluginProvider metadataPluginsProvider, IVersionDataFactory versionDataFactory, IPluginVersionFactory pluginVersionFactory, KarService karService, FeaturesService featuresService, ConfigurationAdmin configurationAdmin, ITelemetryService telemetryService, IDomainStatusMessageFactory domainStatusMessageFactory, IMarketplaceXmlSerializer pluginsSerializer, ISecurityHelper securityHelper, Bundle bundle ) { super( metadataPluginsProvider, versionDataFactory, pluginVersionFactory, karService, featuresService, configurationAdmin, telemetryService, domainStatusMessageFactory ); //initialize dependencies this.setXmlSerializer( pluginsSerializer ); this.setSecurityHelper( securityHelper ); this.setBundle( bundle ); } /** * Called after class is instantiated by Dependency Injector */ public void init() { this.copyKettleFilesToExecutionFolder(); } /** * Called on object destruction by Dependency Injector */ public void destroy() { this.deleteKettleFilesFromExecutionFolder(); } //endregion //region Methods @Override public Map<String, IPlugin> getPlugins() { Map<String, IPlugin> plugins = super.getPlugins(); // remove non BA plugins CollectionUtils.filter( plugins.entrySet(), new Predicate() { @Override public boolean evaluate( Object mapEntry ) { Map.Entry<String, IPlugin> mapEntryCasted = (Map.Entry<String, IPlugin>) mapEntry; return mapEntryCasted.getValue().getType() == MarketEntryType.Platform; } } ); return plugins; } @Override protected boolean hasMarketplacePermission() { Collection<String> authorizedRoles = this.getAuthorizedRoles(); Collection<String> authorizedUsernames = this.getAuthorizedUsernames(); if ( authorizedRoles.isEmpty() && authorizedUsernames.isEmpty() ) { // If it's true, we'll just check if the user is admin return this.getSecurityHelper().isPentahoAdministrator( this.getCurrentSession() ); } Authentication authentication = this.getSecurityHelper().getAuthentication( this.getCurrentSession(), true ); Collection<String> userRoles = this.getRoles( authentication ); String userName = authentication.getName(); return authorizedUsernames.contains( userName ) || CollectionUtils.containsAny( authorizedRoles, userRoles ); } private Collection<String> getRoles( Authentication authentication ) { Collection<String> roles = new ArrayList<>(); for ( GrantedAuthority grantedAuthority : authentication.getAuthorities() ) { roles.add( grantedAuthority.getAuthority() ); } return roles; } @Override protected IPluginVersion getInstalledNonOsgiPluginVersion( IPlugin plugin ) { String versionPath = this.getApplicationContext().getSolutionPath( "system/" + plugin.getId() + "/version.xml" ); FileReader reader = null; try { File file = new File( versionPath ); if ( !file.exists() ) { return null; } reader = new FileReader( versionPath ); IPluginVersion version = this.getXmlSerializer().getInstalledVersion( new InputSource( reader ) ); version.setIsOsgi( false ); return version; } catch ( Exception e ) { e.printStackTrace(); } finally { try { if ( reader != null ) { reader.close(); } } catch ( Exception e ) { // do nothing } } return null; } private boolean isLegacyPlugin( String pluginId ) { String pluginConfigPath = this.getApplicationContext().getSolutionPath( SYSTEM_FOLDER + File.separator + pluginId + File.separator + PLUGIN_XML_FILE ); return ( new File( pluginConfigPath ).isFile() ); } @Override protected Collection<String> getInstalledNonOsgiPluginIds() { Collection<String> plugins = new HashSet<>(); // search and add ids for non-OSGi legacy plugins File systemDir = new File( this.getApplicationContext().getSolutionPath( SYSTEM_FOLDER ) ); String[] dirs = systemDir.list( DirectoryFileFilter.INSTANCE ); for ( String dir : dirs ) { if ( isLegacyPlugin( dir ) && !plugins.contains( dir ) ) { plugins.add( dir ); } } return plugins; } @Override protected void unloadPlugin( IPlugin plugin ) { String pluginId = plugin.getId(); IPluginManager pluginManager = this.getPluginManager( this.getCurrentSession() ); ClassLoader cl = pluginManager.getClassLoader( pluginId ); if ( cl != null && cl instanceof URLClassLoader ) { try { URLClassLoader cl1 = (URLClassLoader) cl; Util.closeURLClassLoader( cl1 ); Method closeMethod = cl1.getClass().getMethod( BaPluginService.CLOSE_METHOD_NAME ); closeMethod.invoke( cl1 ); } catch ( Throwable e ) { if ( e instanceof NoSuchMethodException ) { logger.debug( "Probably running in java 6 so close method on URLClassLoader is not available" ); } else if ( e instanceof IOException ) { logger.error( "Unable to close class loader for plugin. Will try uninstalling plugin anyway", e ); } else { logger.error( "Error while closing class loader", e ); } } } } @Override protected boolean executeNonOsgiInstall( IPlugin plugin, IPluginVersion version ) { try { Result result = this.executeInstallPluginJob( plugin.getId(), version.getDownloadUrl(), version.getSamplesDownloadUrl(), version.getVersion() ); if ( result == null || result.getNrErrors() > 0 ) { return false; } } catch ( KettleException e ) { logger.error( e.getMessage(), e ); return false; } return true; } @Override protected boolean executeNonOsgiUninstall( IPlugin plugin ) { try { Result result = this.executeUninstallPluginJob( plugin.getId() ); if ( result == null || result.getNrErrors() > 0 ) { return false; } } catch ( KettleException e ) { logger.error( e.getMessage(), e ); return false; } return true; } private Result executeInstallPluginJob( String pluginId, String downloadUrl, String samplesDownloadUrl, String availableVersion ) throws UnknownParamException { JobMeta installMeta = this.getInstallJobMeta(); if ( installMeta == null ) { this.getLogger().error( "Unable to find install job meta." ); return null; } Job job = new Job( null, installMeta ); File file = new File( this.getApplicationContext().getSolutionPath( DOWNLOAD_CACHE_FOLDER ) ); file.mkdirs(); file = new File( this.getApplicationContext().getSolutionPath( BACKUP_CACHE_FOLDER ) ); file.mkdirs(); file = new File( this.getApplicationContext().getSolutionPath( STAGING_CACHE_FOLDER ) ); file.mkdirs(); job.getJobMeta().setParameterValue( "downloadUrl", downloadUrl ); if ( samplesDownloadUrl != null ) { job.getJobMeta().setParameterValue( "samplesDownloadUrl", samplesDownloadUrl ); job.getJobMeta().setParameterValue( "samplesDir", "/public/plugin-samples" ); job.getJobMeta().setParameterValue( "samplesTargetDestination", this.getApplicationContext() .getSolutionPath( "plugin-samples/" + pluginId ) ); job.getJobMeta().setParameterValue( "samplesTargetBackup", this.getApplicationContext() .getSolutionPath( BACKUP_CACHE_FOLDER + pluginId + "_samples_" + new Date() .getTime() ) ); job.getJobMeta().setParameterValue( "samplesDownloadDestination", this.getApplicationContext() .getSolutionPath( DOWNLOAD_CACHE_FOLDER + pluginId + "-samples-" + availableVersion + "_" + new Date().getTime() + ".zip" ) ); job.getJobMeta().setParameterValue( "samplesStagingDestination", this.getApplicationContext() .getSolutionPath( "system/plugin-cache/staging_samples" ) ); job.getJobMeta().setParameterValue( "samplesStagingDestinationAndDir", this.getApplicationContext() .getSolutionPath( "system/plugin-cache/staging_samples/" + pluginId ) ); } job.getJobMeta().setParameterValue( "downloadDestination", this.getApplicationContext() .getSolutionPath( "system/plugin-cache/downloads/" + pluginId + "-" + availableVersion + "_" + new Date().getTime() + ".zip" ) ); job.getJobMeta().setParameterValue( "stagingDestination", this.getApplicationContext() .getSolutionPath( STAGING_CACHE_FOLDER ) ); job.getJobMeta().setParameterValue( "stagingDestinationAndDir", this.getApplicationContext() .getSolutionPath( STAGING_CACHE_FOLDER + pluginId ) ); job.getJobMeta().setParameterValue( "targetDestination", this.getApplicationContext() .getSolutionPath( "system/" + pluginId ) ); job.getJobMeta().setParameterValue( "targetBackup", this.getApplicationContext() .getSolutionPath( BACKUP_CACHE_FOLDER + pluginId + "_" + new Date().getTime() ) ); job.copyParametersFrom( job.getJobMeta() ); job.setLogLevel( LogLevel.DETAILED ); job.activateParameters(); job.start(); job.waitUntilFinished(); Result result = job.getResult(); // Execute the selected job. return result; } private Result executeUninstallPluginJob( String pluginId ) throws UnknownParamException { JobMeta uninstallJobMeta = this.getUninstallJobMeta(); if ( uninstallJobMeta == null ) { this.getLogger().error( "Unable to find uninstall job meta." ); return null; } Job job = new Job( null, uninstallJobMeta ); File file = new File( this.getApplicationContext().getSolutionPath( BACKUP_CACHE_FOLDER ) ); file.mkdirs(); String uninstallBackup = this.getApplicationContext().getSolutionPath( BACKUP_CACHE_FOLDER + pluginId + "_" + new Date().getTime() ); job.getJobMeta().setParameterValue( "uninstallLocation", this.getApplicationContext() .getSolutionPath( "system/" + pluginId ) ); job.getJobMeta().setParameterValue( "uninstallBackup", uninstallBackup ); job.getJobMeta().setParameterValue( "samplesDir", "/public/plugin-samples/" + pluginId ); job.copyParametersFrom( job.getJobMeta() ); job.activateParameters(); job.start(); job.waitUntilFinished(); Result result = job.getResult(); // Execute the selected job. return result; } private void copyKettleFilesToExecutionFolder() { Path kettleExecutionFolderPath = this.getAbsoluteKettleExecutionFolderPath(); File targetKettleFilesFolder = new File( kettleExecutionFolderPath.toUri() ); if ( !targetKettleFilesFolder.exists() && !targetKettleFilesFolder.mkdirs() ) { this.getLogger().error( "Failed to create temporary folder for marketplace kettle transformations at " + targetKettleFilesFolder.toString() ); } String kettleResourcesSourcePath = this.getAbsoluteKettleResourcesSourcePath(); Iterable<String> kettleResourcePaths = Collections.list( bundle.getEntryPaths( kettleResourcesSourcePath ) ); for ( String kettleResourcePath : kettleResourcePaths ) { this.writeResourceToFolder( kettleResourcePath, kettleExecutionFolderPath ); } } private void deleteKettleFilesFromExecutionFolder() { URI kettleExecutionFolder = this.getAbsoluteKettleExecutionFolderPath().toUri(); File targetKettleFilesFolder = new File( kettleExecutionFolder ); if ( targetKettleFilesFolder.exists() ) { try { FileUtils.deleteDirectory( targetKettleFilesFolder ); } catch ( IOException e ) { this.getLogger().error( "Unable to delete marketplace temporary kettle execution folder: " + targetKettleFilesFolder.toString(), e ); } } } private void writeResourceToFolder( URL resourceUrl, Path destinationFolder ) { try { InputStream inputStream = resourceUrl.openConnection().getInputStream(); String fileName = FilenameUtils.getName( resourceUrl.toString() ); Path destinationFile = destinationFolder.resolve( fileName ); Files.copy( inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING ); } catch ( IOException e ) { this.getLogger() .error( "Error copying " + resourceUrl.toString() + " to destination folder " + destinationFolder, e ); } } private void writeResourceToFolder( String resourceUrl, Path destinationFolder ) { URL url = this.getBundle().getResource( resourceUrl ); this.writeResourceToFolder( url, destinationFolder ); } private Collection<String> parseStringCollection( String string, String valueSeparator ) { String[] splitString = string.split( valueSeparator ); Collection<String> parsedValues = new ArrayList<>( splitString.length ); for ( String authorizedRole : splitString ) { if ( authorizedRole != null ) { authorizedRole = authorizedRole.trim(); if ( !authorizedRole.isEmpty() ) { parsedValues.add( authorizedRole ); } } } return parsedValues; } //endregion }