/*
* Copyright (C) 2005-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.alfresco.repo.tenant;
import java.io.File;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import javax.transaction.UserTransaction;
import net.sf.acegisecurity.providers.encoding.PasswordEncoder;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.admin.RepoModelDefinition;
import org.alfresco.repo.content.TenantRoutingFileContentStore;
import org.alfresco.repo.dictionary.DictionaryComponent;
import org.alfresco.repo.importer.ImporterBootstrap;
import org.alfresco.repo.node.db.DbNodeServiceImpl;
import org.alfresco.repo.security.authentication.AuthenticationContext;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.site.SiteAVMBootstrap;
import org.alfresco.repo.usage.UserUsageTrackingComponent;
import org.alfresco.repo.workflow.WorkflowDeployer;
import org.alfresco.service.cmr.admin.RepoAdminService;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.alfresco.service.cmr.attributes.AttributeService.AttributeQueryCallback;
import org.alfresco.service.cmr.module.ModuleService;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.view.RepositoryExporterService;
import org.alfresco.service.cmr.workflow.WorkflowDefinition;
import org.alfresco.service.cmr.workflow.WorkflowService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.extensions.surf.util.I18NUtil;
import org.springframework.extensions.surf.util.ParameterCheck;
/**
* MT Admin Service Implementation.
*
*/
public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationContextAware, InitializingBean
{
// Logger
private static Log logger = LogFactory.getLog(MultiTAdminServiceImpl.class);
// Keep hold of the app context
private ApplicationContext ctx;
// Dependencies
private NodeService nodeService;
private DictionaryComponent dictionaryComponent;
private RepoAdminService repoAdminService;
private AuthenticationContext authenticationContext;
private TransactionService transactionService;
private MultiTServiceImpl tenantService;
private AttributeService attributeService;
private PasswordEncoder passwordEncoder;
private TenantRoutingFileContentStore tenantFileContentStore;
private WorkflowService workflowService;
private RepositoryExporterService repositoryExporterService;
private ModuleService moduleService;
private SiteAVMBootstrap siteAVMBootstrap;
private List workflowDeployers = new ArrayList();
private String baseAdminUsername = null;
/*
* Tenant domain/ids are unique strings that are case-insensitive. Tenant ids must be valid filenames.
* They may also map onto domains and hence should allow valid FQDN.
*
* The following PCRE-style
* regex defines a valid label within a FQDN:
*
* ^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$
*
* Less formally:
*
* o Case insensitive
* o First/last character: alphanumeric
* o Interior characters: alphanumeric plus hyphen
* o Minimum length: 2 characters
* o Maximum length: 63 characters
*
* The FQDN (fully qualified domain name) has the following constraints:
*
* o Maximum 255 characters (***)
* o Must contain at least one alpha
*
* Note: (***) Due to various internal restrictions (such as store identifier) we restrict tenant ids to 75 characters.
*/
protected final static String REGEX_VALID_DNS_LABEL = "^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$";
protected final static String REGEX_CONTAINS_ALPHA = "^(.*)[a-zA-Z](.*)$";
protected final static int MAX_LEN = 75;
public void setNodeService(DbNodeServiceImpl dbNodeService)
{
this.nodeService = dbNodeService;
}
public void setDictionaryComponent(DictionaryComponent dictionaryComponent)
{
this.dictionaryComponent = dictionaryComponent;
}
public void setRepoAdminService(RepoAdminService repoAdminService)
{
this.repoAdminService = repoAdminService;
}
public void setAuthenticationContext(AuthenticationContext authenticationContext)
{
this.authenticationContext = authenticationContext;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setTenantService(MultiTServiceImpl tenantService)
{
this.tenantService = tenantService;
}
public void setAttributeService(AttributeService attributeService)
{
this.attributeService = attributeService;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder)
{
this.passwordEncoder = passwordEncoder;
}
public void setTenantFileContentStore(TenantRoutingFileContentStore tenantFileContentStore)
{
this.tenantFileContentStore = tenantFileContentStore;
}
public void setWorkflowService(WorkflowService workflowService)
{
this.workflowService = workflowService;
}
public void setRepositoryExporterService(RepositoryExporterService repositoryExporterService)
{
this.repositoryExporterService = repositoryExporterService;
}
/**
* @deprecated see setWorkflowDeployers
*/
public void setWorkflowDeployer(WorkflowDeployer workflowDeployer)
{
// NOOP
logger.warn(WARN_MSG);
}
public void setModuleService(ModuleService moduleService)
{
this.moduleService = moduleService;
}
public void setSiteAVMBootstrap(SiteAVMBootstrap siteAVMBootstrap)
{
this.siteAVMBootstrap = siteAVMBootstrap;
}
public void setBaseAdminUsername(String baseAdminUsername)
{
this.baseAdminUsername = baseAdminUsername;
}
public static final String PROTOCOL_STORE_USER = "user";
public static final String PROTOCOL_STORE_WORKSPACE = "workspace";
public static final String PROTOCOL_STORE_SYSTEM = "system";
public static final String PROTOCOL_STORE_ARCHIVE = "archive";
public static final String STORE_BASE_ID_USER = "alfrescoUserStore";
public static final String STORE_BASE_ID_SYSTEM = "system";
public static final String STORE_BASE_ID_VERSION1 = "lightWeightVersionStore"; // deprecated
public static final String STORE_BASE_ID_VERSION2 = "version2Store";
public static final String STORE_BASE_ID_SPACES = "SpacesStore";
public static final String TENANTS_ATTRIBUTE_PATH = "alfresco-tenants";
public static final String TENANT_ATTRIBUTE_ENABLED = "enabled";
public static final String TENANT_ATTRIBUTE_ROOT_CONTENT_STORE_DIR = "rootContentStoreDir";
private List tenantDeployers = new ArrayList();
private static final String WARN_MSG = "system.mt.warn.upgrade_mt_admin_context";
public void afterPropertiesSet() throws Exception
{
// for upgrade/backwards compatibility with 3.0.x (mt-admin-context.xml)
if (baseAdminUsername == null)
{
logger.warn(I18NUtil.getMessage(WARN_MSG));
}
// for upgrade/backwards compatibility with 3.0.x (mt-admin-context.xml)
if (siteAVMBootstrap == null)
{
logger.warn(I18NUtil.getMessage(WARN_MSG));
siteAVMBootstrap = (SiteAVMBootstrap) ctx.getBean("siteAVMBootstrap");
}
PropertyCheck.mandatory(this, "NodeService", nodeService);
PropertyCheck.mandatory(this, "DictionaryComponent", dictionaryComponent);
PropertyCheck.mandatory(this, "RepoAdminService", repoAdminService);
PropertyCheck.mandatory(this, "TransactionService", transactionService);
PropertyCheck.mandatory(this, "TenantService", tenantService);
PropertyCheck.mandatory(this, "AttributeService", attributeService);
PropertyCheck.mandatory(this, "PasswordEncoder", passwordEncoder);
PropertyCheck.mandatory(this, "TenantFileContentStore", tenantFileContentStore);
PropertyCheck.mandatory(this, "WorkflowService", workflowService);
PropertyCheck.mandatory(this, "RepositoryExporterService", repositoryExporterService);
PropertyCheck.mandatory(this, "moduleService", moduleService);
PropertyCheck.mandatory(this, "siteAVMBootstrap", siteAVMBootstrap);
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
this.ctx = applicationContext;
}
public void startTenants()
{
AuthenticationUtil.setMtEnabled(true);
// initialise the tenant admin service and status of tenants (using attribute service)
// note: this requires that the repository schema has already been initialised
// register dictionary - to allow enable/disable tenant callbacks
register(dictionaryComponent);
// register file store - to allow enable/disable tenant callbacks
// note: tenantFileContentStore must be registed before dictionaryRepositoryBootstrap
register(tenantFileContentStore, 0);
UserTransaction userTransaction = transactionService.getUserTransaction();
try
{
authenticationContext.setSystemUserAsCurrentUser();
userTransaction.begin();
// bootstrap Tenant Service internal cache
List tenants = getAllTenants();
int enabledCount = 0;
int disabledCount = 0;
for (Tenant tenant : tenants)
{
if (tenant.isEnabled())
{
// this will also call tenant deployers registered so far ...
enableTenant(tenant.getTenantDomain(), true);
enabledCount++;
}
else
{
// explicitly disable, without calling disableTenant callback
disableTenant(tenant.getTenantDomain(), false);
disabledCount++;
}
}
tenantService.register(this); // callback to refresh tenantStatus cache
userTransaction.commit();
if (logger.isInfoEnabled())
{
logger.info(String.format("Alfresco Multi-Tenant startup - %d enabled tenants, %d disabled tenants",
enabledCount, disabledCount));
}
}
catch(Throwable e)
{
// rollback the transaction
try { if (userTransaction != null) {userTransaction.rollback();} } catch (Exception ex) {}
throw new AlfrescoRuntimeException("Failed to bootstrap tenants", e);
}
finally
{
authenticationContext.clearCurrentSecurityContext();
}
}
public void stopTenants()
{
tenantDeployers.clear();
tenantDeployers = null;
}
/**
* @see TenantAdminService.createTenant()
*/
public void createTenant(final String tenantDomain, final char[] tenantAdminRawPassword)
{
createTenant(tenantDomain, tenantAdminRawPassword, null);
}
/**
* @see TenantAdminService.createTenant()
*/
public void createTenant(final String tenantDomain, final char[] tenantAdminRawPassword, String rootContentStoreDir)
{
ParameterCheck.mandatory("tenantAdminRawPassword", tenantAdminRawPassword);
initTenant(tenantDomain, rootContentStoreDir);
try
{
// note: runAs would cause auditable property "creator" to be "admin" instead of "System@xxx"
AuthenticationUtil.pushAuthentication();
AuthenticationUtil.setFullyAuthenticatedUser(getSystemUser(tenantDomain));
dictionaryComponent.init();
tenantFileContentStore.init();
// create tenant-specific stores
ImporterBootstrap userImporterBootstrap = (ImporterBootstrap)ctx.getBean("userBootstrap-mt");
bootstrapUserTenantStore(userImporterBootstrap, tenantDomain, tenantAdminRawPassword);
ImporterBootstrap systemImporterBootstrap = (ImporterBootstrap)ctx.getBean("systemBootstrap-mt");
bootstrapSystemTenantStore(systemImporterBootstrap, tenantDomain);
// deprecated
ImporterBootstrap versionImporterBootstrap = (ImporterBootstrap)ctx.getBean("versionBootstrap-mt");
bootstrapVersionTenantStore(versionImporterBootstrap, tenantDomain);
ImporterBootstrap version2ImporterBootstrap = (ImporterBootstrap)ctx.getBean("version2Bootstrap-mt");
bootstrapVersionTenantStore(version2ImporterBootstrap, tenantDomain);
ImporterBootstrap spacesArchiveImporterBootstrap = (ImporterBootstrap)ctx.getBean("spacesArchiveBootstrap-mt");
bootstrapSpacesArchiveTenantStore(spacesArchiveImporterBootstrap, tenantDomain);
ImporterBootstrap spacesImporterBootstrap = (ImporterBootstrap)ctx.getBean("spacesBootstrap-mt");
bootstrapSpacesTenantStore(spacesImporterBootstrap, tenantDomain);
siteAVMBootstrap.bootstrap();
// notify listeners that tenant has been created & hence enabled
for (TenantDeployer tenantDeployer : tenantDeployers)
{
tenantDeployer.onEnableTenant();
}
// bootstrap workflows
for (WorkflowDeployer workflowDeployer : workflowDeployers)
{
workflowDeployer.init();
}
// bootstrap modules (if any)
moduleService.startModules();
}
finally
{
AuthenticationUtil.popAuthentication();
}
logger.info("Tenant created: " + tenantDomain);
}
/**
* Export tenant - equivalent to the tenant admin running a 'complete repo' export from the Web Client Admin
*/
public void exportTenant(final String tenantDomain, final File directoryDestination)
{
AuthenticationUtil.runAs(new RunAsWork