package com.atlassian.labs.speakeasy;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.labs.speakeasy.external.PluginType;
import com.atlassian.labs.speakeasy.external.SpeakeasyService;
import com.atlassian.labs.speakeasy.external.UnauthorizedAccessException;
import com.atlassian.labs.speakeasy.manager.*;
import com.atlassian.labs.speakeasy.model.*;
import com.atlassian.labs.speakeasy.product.ProductAccessor;
import com.atlassian.labs.speakeasy.util.FeedBuilder;
import com.atlassian.labs.speakeasy.util.exec.KeyedSyncExecutor;
import com.atlassian.labs.speakeasy.util.exec.Operation;
import com.atlassian.plugin.ModuleDescriptor;
import com.atlassian.plugin.Plugin;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.webresource.UrlMode;
import com.atlassian.plugin.webresource.WebResourceManager;
import com.atlassian.sal.api.ApplicationProperties;
import com.atlassian.sal.api.user.UserManager;
import com.google.common.base.Predicate;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;
/**
*
*/
public class SpeakeasyServiceImpl implements SpeakeasyService
{
private final ApplicationProperties applicationProperties;
private final PluginAccessor pluginAccessor;
private final PluginSystemManager pluginSystemManager;
private final ProductAccessor productAccessor;
private final BundleContext bundleContext;
private final PermissionManager permissionManager;
private final UserManager userManager;
private final SettingsManager settingsManager;
private final WebResourceManager webResourceManager;
private final ModuleDescriptor unknownScreenshotDescriptor;
private final ExtensionOperationManager extensionOperationManager;
private final KeyedSyncExecutor<UserExtension, String> exec;
private final ExtensionManager extensionManager;
private static final Logger log = LoggerFactory.getLogger(SpeakeasyServiceImpl.class);
private final SearchManager searchManager;
public SpeakeasyServiceImpl(PluginAccessor pluginAccessor, PluginSystemManager pluginSystemManager, ProductAccessor productAccessor, BundleContext bundleContext, PermissionManager permissionManager, UserManager userManager, SettingsManager settingsManager, ApplicationProperties applicationProperties, WebResourceManager webResourceManager, ExtensionOperationManager extensionOperationManager, final ExtensionManager extensionManager, SearchManager searchManager)
{
this.pluginAccessor = pluginAccessor;
this.pluginSystemManager = pluginSystemManager;
this.productAccessor = productAccessor;
this.bundleContext = bundleContext;
this.permissionManager = permissionManager;
this.userManager = userManager;
this.settingsManager = settingsManager;
this.applicationProperties = applicationProperties;
this.webResourceManager = webResourceManager;
this.extensionManager = extensionManager;
this.extensionOperationManager = extensionOperationManager;
this.searchManager = searchManager;
this.exec = new KeyedSyncExecutor<UserExtension, String>()
{
@Override
protected UserExtension getTarget(String pluginKey, String user) throws Exception
{
return getRemotePlugin(pluginKey, user);
}
@Override
protected void handleException(String pluginKey, Exception ex)
{
if (ex instanceof PluginOperationFailedException ||
ex instanceof UnauthorizedAccessException)
{
throw (RuntimeException) ex;
}
else
{
throw new PluginOperationFailedException(ex.getMessage(), ex, pluginKey);
}
}
@Override
protected void afterSuccessfulOperation(UserExtension target, Object result)
{
if (result instanceof String)
{
extensionManager.resetExtension((String) result);
}
else if (result instanceof List)
{
extensionManager.resetExtensions((List<String>)result);
}
}
};
this.unknownScreenshotDescriptor = pluginAccessor.getPluginModule("com.atlassian.labs.speakeasy-plugin:shared");
}
public UserPlugins getRemotePluginList(String userName, String... modifiedKeys) throws UnauthorizedAccessException
{
return getRemotePluginList(userName, asList(modifiedKeys));
}
public UserPlugins getRemotePluginList(String userName, List<String> modifiedKeys) throws UnauthorizedAccessException
{
validateAccess(userName);
Iterable<UserExtension> plugins = extensionManager.getAllUserExtensions(userName);
UserPlugins userPlugins = new UserPlugins(filter(plugins, new AuthorAccessFilter(permissionManager.canAuthorExtensions(userName))));
userPlugins.setUpdated(modifiedKeys);
return userPlugins;
}
public String getPluginFeed(String userName) throws UnauthorizedAccessException
{
validateAccess(userName);
List<Plugin> plugins = extensionManager.getAllExtensionPlugins();
return new FeedBuilder(plugins, bundleContext.getBundles()).
serverName(applicationProperties.getDisplayName()).
serverBaseUrl(applicationProperties.getBaseUrl()).
profilePath(productAccessor.getProfilePath()).
build();
}
public boolean doesPluginExist(String pluginKey)
{
return pluginAccessor.getPlugin(pluginKey) != null;
}
public UserExtension getRemotePlugin(String pluginKey, String userName) throws PluginOperationFailedException, UnauthorizedAccessException
{
validateAccess(userName);
return extensionManager.getUserExtension(pluginKey, userName);
}
private Plugin getPlugin(String pluginKey)
{
validatePluginExists(pluginKey);
return pluginAccessor.getPlugin(pluginKey);
}
public List<String> enableExtension(final String pluginKey, final String user) throws UnauthorizedAccessException
{
return enableExtension(pluginKey, user, true);
}
private List<String> enableExtension(final String pluginKey, final String user, final boolean sendNotification) throws UnauthorizedAccessException
{
validateAccess(user);
validatePluginExists(pluginKey);
List<String> keys = exec.forKey(pluginKey, user, new Operation<UserExtension,List<String>>()
{
public List<String> operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "enable", repo.isCanEnable(), user);
return extensionOperationManager.enable(repo, user, sendNotification);
}
});
log.info("Allowed '{}' to access Speakeasy extension '{}'", user, pluginKey);
return keys;
}
public String disableExtension(final String pluginKey, final String user) throws UnauthorizedAccessException
{
validateAccess(user);
validatePluginExists(pluginKey);
String key = exec.forKey(pluginKey, user, new Operation<UserExtension,String>()
{
public String operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "disable", repo.isCanDisable(), user);
return extensionOperationManager.disable(repo, user);
}
});
log.info("Disallowed '{}' to access Speakeasy extension '{}'", user, pluginKey);
return key;
}
public void disableAllExtensions(final String user) throws UnauthorizedAccessException
{
validateAccess(user);
List<String> enabledKeys = extensionOperationManager.findAllEnabledExtensions(user);
for (String key : enabledKeys)
{
exec.forKey(key, user, new Operation<UserExtension,Void>()
{
public Void operateOn(UserExtension repo) throws Exception
{
extensionOperationManager.disable(repo, user);
return null;
}
});
}
extensionOperationManager.saveEnabledPlugins(enabledKeys, user);
log.info("Disallowed '{}' to access all Speakeasy extensions", user);
}
public void restoreAllExtensions(final String user) throws UnauthorizedAccessException
{
validateAccess(user);
// do we care if they've enabled extensions since they unsubscribed?
List<String> keysToRestore = extensionOperationManager.getEnabledPlugins(user);
for (String key : keysToRestore)
{
enableExtension(key, user, false);
}
log.info("Restored '{}' access to all Speakeasy extensions", user);
}
public UserPlugins uninstallPlugin(String pluginKey, final String user) throws PluginOperationFailedException, UnauthorizedAccessException
{
validateAuthor(user);
validatePluginExists(pluginKey);
List<String> keysModified = exec.forKey(pluginKey, user, new Operation<UserExtension,List<String>>()
{
public List<String> operateOn(final UserExtension repo) throws Exception
{
return extensionOperationManager.uninstallExtension(repo, user, new Operation<String,Void>()
{
public Void operateOn(String enablePluginKey) throws Exception
{
validateAccessType(repo, "uninstall", repo.isCanUninstall(), user);
enableExtension(enablePluginKey, user);
return null;
}
});
}
});
log.info("Uninstalled extension '{}' by user '{}'", pluginKey, user);
return getRemotePluginList(user, keysModified);
}
public UserPlugins fork(String pluginKey, final String user, final String description) throws PluginOperationFailedException, UnauthorizedAccessException
{
validateAuthor(user);
validatePluginExists(pluginKey);
List<String> keysModified = exec.forKey(pluginKey, user, new Operation<UserExtension, List<String>>()
{
public List<String> operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "fork", repo.isCanFork(), user);
return extensionOperationManager.forkExtension(repo, user, description);
}
});
log.info("Forked '{}' extension by '{}'", pluginKey, user);
return getRemotePluginList(user, keysModified);
}
public File getPluginAsProject(String pluginKey, String user) throws UnauthorizedAccessException
{
try
{
validateAuthor(user);
UserExtension plugin = getRemotePlugin(pluginKey, user);
validateAccessType(plugin, "download", plugin.isCanDownload(), user);
return pluginSystemManager.getPluginAsProject(pluginKey, plugin.getPluginType(), user);
}
catch (PluginOperationFailedException ex)
{
throw ex;
}
catch (RuntimeException ex)
{
throw new PluginOperationFailedException(ex.getMessage(), ex, pluginKey);
}
}
public File getPluginArtifact(String pluginKey, String user) throws UnauthorizedAccessException
{
try
{
validateAuthor(user);
UserExtension plugin = getRemotePlugin(pluginKey, user);
validateAccessType(plugin, "download", plugin.isCanDownload(), user);
return pluginSystemManager.getPluginArtifact(pluginKey, plugin.getPluginType());
}
catch (PluginOperationFailedException ex)
{
throw ex;
}
catch (RuntimeException ex)
{
throw new PluginOperationFailedException(ex.getMessage(), ex, pluginKey);
}
}
public List<String> getPluginFileNames(String pluginKey, String user) throws UnauthorizedAccessException
{
try
{
validateAuthor(user);
validatePluginExists(pluginKey);
UserExtension plugin = getRemotePlugin(pluginKey, user);
return newArrayList(filter(pluginSystemManager.getPluginFileNames(pluginKey, plugin.getPluginType()), new Predicate<String>()
{
public boolean apply(String input)
{
return !input.contains("-min.");
}
}));
}
catch (PluginOperationFailedException ex)
{
throw ex;
}
catch (RuntimeException ex)
{
throw new PluginOperationFailedException(ex.getMessage(), ex, pluginKey);
}
}
public Object getPluginFile(String pluginKey, String fileName, String user) throws UnauthorizedAccessException
{
try
{
validateAuthor(user);
validatePluginExists(pluginKey);
UserExtension plugin = getRemotePlugin(pluginKey, user);
return pluginSystemManager.getPluginFile(pluginKey, plugin.getPluginType(), fileName);
}
catch (PluginOperationFailedException ex)
{
throw ex;
}
catch (RuntimeException ex)
{
throw new PluginOperationFailedException(ex.getMessage(), ex, pluginKey);
}
}
public UserExtension saveAndRebuild(String pluginKey, final String fileName, final String contents, final String user) throws UnauthorizedAccessException
{
validateAuthor(user);
validatePluginExists(pluginKey);
String installedKey = exec.forKey(pluginKey, user, new Operation<UserExtension, String>()
{
public String operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "edit", repo.isCanEdit(), user);
return extensionOperationManager.saveAndRebuild(repo, fileName, contents, user);
}
});
log.info("Saved and rebuilt extension '{}' by user '{}'", pluginKey, user);
return getRemotePlugin(installedKey, user);
}
public UserPlugins favorite(final String pluginKey, final String user) throws UnauthorizedAccessException
{
validateAccess(user);
validatePluginExists(pluginKey);
String favoritedPluginKey = exec.forKey(pluginKey, user, new Operation<UserExtension, String>()
{
public String operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "favorite", repo.isCanFavorite(), user);
return extensionOperationManager.favorite(repo, user);
}
});
log.info("Favorited '{}' by user '{}'", favoritedPluginKey, user);
return getRemotePluginList(user, favoritedPluginKey);
}
public UserPlugins unfavorite(final String pluginKey, final String user) throws UnauthorizedAccessException
{
validateAccess(user);
validatePluginExists(pluginKey);
String unfavoritedPluginkey = exec.forKey(pluginKey, user, new Operation<UserExtension, String>()
{
public String operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "unfavorite", !repo.isCanFavorite(), user);
return extensionOperationManager.unfavorite(repo, user);
}
});
log.info("Unfavorited '{}' by user '{}'", unfavoritedPluginkey, user);
return getRemotePluginList(user, unfavoritedPluginkey);
}
public UserPlugins enableGlobally(String pluginKey, final String user)
{
validateAccess(user);
validateAdmin(user);
validatePluginExists(pluginKey);
exec.forKey(pluginKey, user, new Operation<UserExtension, Void>()
{
public Void operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "enable globally", repo.isCanEnableGlobally(), user);
extensionOperationManager.enableGlobally(repo, user);
return null;
}
});
log.info("Enabled extension '{}' globally by user '{}'", pluginKey, user);
return getRemotePluginList(user, pluginKey);
}
public UserPlugins disableGlobally(String pluginKey, final String user)
{
validateAccess(user);
validateAdmin(user);
validatePluginExists(pluginKey);
exec.forKey(pluginKey, user, new Operation<UserExtension, Void>()
{
public Void operateOn(UserExtension repo) throws Exception
{
validateAccessType(repo, "disable globally", repo.isCanDisableGlobally(), user);
extensionOperationManager.disableGlobally(repo, user);
return null;
}
});
log.info("Disabled extension '{}' globally by user '{}'", pluginKey, user);
return getRemotePluginList(user, pluginKey);
}
public void sendFeedback(final String pluginKey, final Feedback feedback, final String user) throws UnauthorizedAccessException
{
validateAccess(user);
validatePluginExists(pluginKey);
exec.forKey(pluginKey, user, new Operation<UserExtension, String>()
{
public String operateOn(UserExtension repo) throws Exception
{
extensionOperationManager.sendFeedback(repo, feedback, user);
return null;
}
});
log.info("Sent feedback for '{}' by user '{}'", pluginKey, user);
}
public void reportBroken(final String pluginKey, final Feedback feedback, final String user) throws UnauthorizedAccessException
{
validateAccess(user);
validatePluginExists(pluginKey);
exec.forKey(pluginKey, user, new Operation<UserExtension, String>()
{
public String operateOn(UserExtension repo) throws Exception
{
extensionOperationManager.reportBroken(repo, feedback, user);
return null;
}
});
log.info("Send broken report for '{}' by user '{}'", pluginKey, user);
}
public UserPlugins installPlugin(File uploadedFile, String user) throws UnauthorizedAccessException
{
return installPlugin(uploadedFile, null, user);
}
public UserPlugins installPlugin(final File uploadedFile, final String expectedPluginKey, final String user) throws UnauthorizedAccessException
{
validateAuthor(user);
String installedPluginKey = exec.forKey(expectedPluginKey, user, new Operation<UserExtension, String>()
{
public String operateOn(UserExtension ext) throws Exception
{
if (ext != null)
{
validateAccessType(ext, "upgrade", ext.isCanEdit(), user);
}
return extensionOperationManager.install(ext, uploadedFile, user);
}
});
log.info("Installed extension '{}' by user '{}'", installedPluginKey, user);
return getRemotePluginList(user, installedPluginKey);
}
public UserPlugins createExtension(String pluginKey, PluginType pluginType, String remoteUser, String description, String name) throws UnauthorizedAccessException
{
validateAuthor(remoteUser);
validatePluginDoesNotExist(pluginKey);
try
{
pluginSystemManager.createExtension(pluginType, pluginKey, remoteUser, description, name);
List<String> modifiedKeys = new ArrayList<String>();
modifiedKeys.add(pluginKey);
log.info("Created extension '{}' by user '{}'", pluginKey, remoteUser);
return getRemotePluginList(remoteUser, modifiedKeys);
}
catch (PluginOperationFailedException ex)
{
throw ex;
}
catch (RuntimeException ex)
{
throw new PluginOperationFailedException(ex.getMessage(), ex, pluginKey);
}
}
public SearchResults search(String searchQuery, String remoteUsername)
{
// restrict to admins due to very inefficient search implementation
validateAdmin(remoteUsername);
return searchManager.search(searchQuery);
}
public Settings getSettings(String userName) throws UnauthorizedAccessException
{
validateAdmin(userName);
return settingsManager.getSettings();
}
public boolean doesAnyGroupHaveAccess()
{
return !settingsManager.getSettings().getAccessGroups().isEmpty();
}
public Settings saveSettings(Settings settings, String userName) throws UnauthorizedAccessException
{
validateAdmin(userName);
Settings savedSettings = settingsManager.setSettings(settings);
log.info("Saved administration settings by user '{}'", userName);
return savedSettings;
}
public boolean canAccessSpeakeasy(String username)
{
return permissionManager.canAccessSpeakeasy(username);
}
public boolean canAuthorExtensions(String user)
{
return permissionManager.canAuthorExtensions(user);
}
public String getScreenshotUrl(String pluginKey, String user) throws UnauthorizedAccessException
{
validateAccess(user);
Plugin plugin = getPlugin(pluginKey);
ModuleDescriptor<?> screenshotDescriptor = plugin.getModuleDescriptor("screenshot");
if (screenshotDescriptor == null)
{
screenshotDescriptor = unknownScreenshotDescriptor;
}
return webResourceManager.getStaticPluginResource(screenshotDescriptor, "screenshot.png", UrlMode.ABSOLUTE);
}
private void validateAccess(String userName) throws UnauthorizedAccessException
{
if (!permissionManager.canAccessSpeakeasy(userName))
{
log.warn("Unauthorized Speakeasy access by '" + userName + "'");
throw new UnauthorizedAccessException(userName, "Cannot access Speakeasy due to lack of permissions");
}
}
private void validateAuthor(String userName) throws UnauthorizedAccessException
{
if (!permissionManager.canAuthorExtensions(userName))
{
log.warn("Unauthorized Speakeasy author access by '" + userName + "'");
throw new UnauthorizedAccessException(userName, "Cannot access Speakeasy due to lack of permissions");
}
}
private void validateAccessType(Extension ext, String type, boolean allowed, String user) throws UnauthorizedAccessException
{
if (!allowed)
{
log.warn("Unauthorized Speakeasy " + type + " access by '" + user + "' for extension '" + ext.getKey() + "'");
throw new UnauthorizedAccessException(user, "Cannot " + type + " Speakeasy extension '" + ext.getName() + "' due to lack of permissions");
}
}
private void validateAdmin(String userName) throws UnauthorizedAccessException
{
if (!userManager.isAdmin(userName))
{
log.warn("Unauthorized Speakeasy admin access by '" + userName + "'");
throw new UnauthorizedAccessException(userName, "Cannot access Speakeasy due to lack of permissions");
}
}
public void validatePluginExists(String pluginKey) throws PluginOperationFailedException
{
if (pluginAccessor.getPlugin(pluginKey) == null)
{
throw new PluginOperationFailedException("Extension '" + pluginKey + "' doesn't exists", null);
}
}
public void validatePluginDoesNotExist(String pluginKey) throws PluginOperationFailedException
{
if (pluginAccessor.getPlugin(pluginKey) != null)
{
throw new PluginOperationFailedException("Extension '" + pluginKey + "' already exists", null);
}
}
public boolean canEditPlugin(String name, String remoteUsername)
{
try
{
return canAuthorExtensions(remoteUsername) && getRemotePlugin(name, remoteUsername).isCanEdit();
}
catch (UnauthorizedAccessException e)
{
return false;
}
}
private static class AuthorAccessFilter implements Predicate<Extension>
{
private final boolean hasAuthorAccess;
public AuthorAccessFilter(boolean hasAuthorAccess)
{
this.hasAuthorAccess = hasAuthorAccess;
}
public boolean apply(Extension input)
{
return !input.isFork() || hasAuthorAccess;
}
}
}