/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * 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 . * #L% */ package org.alfresco.repo.dictionary; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.domain.qname.QNameDAO; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.tenant.Tenant; import org.alfresco.repo.tenant.TenantAdminService; import org.alfresco.repo.tenant.TenantService; import org.alfresco.repo.tenant.TenantUtil; import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.repo.workflow.BPMEngineRegistry; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.ConstraintDefinition; import org.alfresco.service.cmr.dictionary.DictionaryException; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.ModelDefinition; import org.alfresco.service.cmr.dictionary.NamespaceDefinition; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.dictionary.TypeDefinition; import org.alfresco.service.cmr.workflow.WorkflowDefinition; import org.alfresco.service.cmr.workflow.WorkflowService; import org.alfresco.service.cmr.workflow.WorkflowTaskDefinition; import org.alfresco.service.namespace.NamespaceException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.dao.DataIntegrityViolationException; /** * Model change validation covering model deletes, model constituent changes e.g. property deletes, * additions, etc. * * @author sglover */ public class ModelValidatorImpl implements ModelValidator { private static final Log logger = LogFactory.getLog(ModelValidatorImpl.class); private DictionaryDAO dictionaryDAO; private DictionaryService dictionaryService; private QNameDAO qnameDAO; private NamespaceService namespaceService; private TransactionService transactionService; private WorkflowService workflowService; private TenantService tenantService; private TenantAdminService tenantAdminService; private boolean enforceTenantInNamespace = false; public void setEnforceTenantInNamespace(boolean enforceTenantInNamespace) { this.enforceTenantInNamespace = enforceTenantInNamespace; } public void setTransactionService(TransactionService transactionService) { this.transactionService = transactionService; } public void setQnameDAO(QNameDAO qnameDAO) { this.qnameDAO = qnameDAO; } public void setDictionaryDAO(DictionaryDAO dictionaryDAO) { this.dictionaryDAO = dictionaryDAO; } public void setNamespaceService(NamespaceService namespaceService) { this.namespaceService = namespaceService; } public void setWorkflowService(WorkflowService workflowService) { this.workflowService = workflowService; } public void setTenantService(TenantService tenantService) { this.tenantService = tenantService; } public void setTenantAdminService(TenantAdminService tenantAdminService) { this.tenantAdminService = tenantAdminService; } public void setDictionaryService(DictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } private void checkCustomModelNamespace(M2Model model, String tenantDomain) { if(tenantDomain != null && !tenantDomain.equals("") && enforceTenantInNamespace) { // check only for "real" tenants for(M2Namespace namespace : model.getNamespaces()) { String namespaceURI = namespace.getUri(); if(namespaceURI.indexOf(tenantDomain) == -1) { throw new DictionaryException("Namespace " + namespaceURI + " does not contain the tenant " + tenantDomain); } } } } private boolean canDeleteModel(Collection namespaceDefs, Collection typeDefs, Collection aspectDefs, Tenant tenant) { boolean canDelete = true; String tenantDomain = "for tenant [" + (tenant == null ? TenantService.DEFAULT_DOMAIN : tenant.getTenantDomain()) + "]"; List workflowDefs = workflowService.getDefinitions(); if (workflowDefs.size() > 0) { if (namespaceDefs.size() > 0) { // check workflow namespace usage for (WorkflowDefinition workflowDef : workflowDefs) { String workflowDefName = workflowDef.getName(); String workflowNamespaceURI = null; try { workflowNamespaceURI = QName.createQName(BPMEngineRegistry.getLocalId(workflowDefName), namespaceService).getNamespaceURI(); } catch (NamespaceException ne) { logger.warn("Skipped workflow when validating model delete - unknown namespace: "+ne); continue; } for (NamespaceDefinition namespaceDef : namespaceDefs) { if (workflowNamespaceURI.equals(namespaceDef.getUri())) { logger.warn("Failed to validate model delete" + tenantDomain + " - found workflow process definition " + workflowDefName + " using model namespace '" + namespaceDef.getUri() + "'"); canDelete = false; } } } } } // check for type usages outer: for (TypeDefinition type : typeDefs) { try { validateDeleteClass(tenant, type); } catch(ModelInUseException e) { canDelete = false; break outer; } catch(ModelNotInUseException e) { // ok, continue } } // check for aspect usages outer: for (AspectDefinition aspect : aspectDefs) { try { validateDeleteClass(tenant, aspect); } catch(ModelInUseException e) { canDelete = false; break outer; } catch(ModelNotInUseException e) { // ok, continue } } return canDelete; } private void validateDeleteClass(final Tenant tenant, final ClassDefinition classDef) { final String classType = "TYPE"; final QName className = classDef.getName(); String tenantDomain = "for tenant [" + (tenant == null ? TenantService.DEFAULT_DOMAIN : tenant.getTenantDomain()) + "]"; // We need a separate transaction to do the qname delete "check" transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() { @Override public Void execute() throws Throwable { try { // The class QName may not have been created in the database if no // properties have been created that use it, so check first and then // try to delete it. if(qnameDAO.getQName(className) != null) { qnameDAO.deleteQName(className); } throw new ModelNotInUseException("Class " + className + " not in use"); } catch(DataIntegrityViolationException e) { // catch data integrity violation e.g. foreign key constraint exception logger.debug(e); throw new ModelInUseException("Cannot delete model, class " + className + " is in use"); } } }, false, true); // check against workflow task usage for (WorkflowDefinition workflowDef : workflowService.getDefinitions()) { for (WorkflowTaskDefinition workflowTaskDef : workflowService.getTaskDefinitions(workflowDef.getId())) { TypeDefinition workflowTypeDef = workflowTaskDef.metadata; if (workflowTypeDef.getName().equals(className)) { throw new AlfrescoRuntimeException("Failed to validate model delete" + tenantDomain + " - found task definition in workflow " + workflowDef.getName() + " with " + classType + " '" + className + "'"); } } } } private void validateDeleteProperty(QName modelName, QName propertyQName, boolean sharedModel) { String tenantDomain = TenantService.DEFAULT_DOMAIN; if (sharedModel) { tenantDomain = " for tenant [" + tenantService.getCurrentUserDomain() + "]"; } PropertyDefinition prop = dictionaryDAO.getProperty(propertyQName); if(prop != null && prop.getName().equals(propertyQName) && prop.getModel().getName().equals(modelName)) { validateDeleteProperty(tenantDomain, prop); } else { throw new AlfrescoRuntimeException("Cannot delete model " + modelName + " in tenant " + tenantDomain + " - property definition '" + propertyQName + "' not defined in model '" + modelName + "'"); } } private void validateDeleteProperty(final String tenantDomain, final PropertyDefinition propDef) { final QName propName = propDef.getName(); // We need a separate transaction to do the qname delete "check" transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() { @Override public Void execute() throws Throwable { return TenantUtil.runAsTenant(new TenantRunAsWork() { @Override public Void doWork() throws Exception { try { // The property QName may not have been created in the database if no // properties have been created that use it, so check first and then // try to delete it. if(qnameDAO.getQName(propName) != null) { qnameDAO.deleteQName(propName); } } catch(DataIntegrityViolationException e) { // catch data integrity violation e.g. foreign key constraint exception logger.debug(e); throw new ModelInUseException("Failed to validate property delete, property " + propName + " is in use"); } return null; } }, tenantDomain); } }, false, true); } // validate delete of a referencable constraint def private void validateDeleteConstraint(CompiledModel compiledModel, QName constraintName, boolean sharedModel) { String tenantDomain = TenantService.DEFAULT_DOMAIN; if (sharedModel) { tenantDomain = " for tenant [" + tenantService.getCurrentUserDomain() + "]"; } Set referencedBy = new HashSet(0); // check for references to constraint definition // note: could be anon prop constraint (if no referenceable constraint) Collection allModels = dictionaryDAO.getModels(); for (QName model : allModels) { Collection propDefs = null; if (compiledModel.getModelDefinition().getName().equals(model)) { // TODO deal with multiple pending model updates propDefs = compiledModel.getProperties(); } else { propDefs = dictionaryDAO.getProperties(model); } for (PropertyDefinition propDef : propDefs) { for (ConstraintDefinition conDef : propDef.getConstraints()) { if (constraintName.equals(conDef.getRef())) { referencedBy.add(conDef.getName()); } } } } if (referencedBy.size() == 1) { throw new AlfrescoRuntimeException("Failed to validate constraint delete" + tenantDomain + " - constraint definition '" + constraintName + "' is being referenced by '" + referencedBy.toArray()[0] + "' property constraint"); } else if (referencedBy.size() > 1) { throw new AlfrescoRuntimeException("Failed to validate constraint delete" + tenantDomain + " - constraint definition '" + constraintName + "' is being referenced by " + referencedBy.size() + " property constraints"); } } /** * {@inheritDoc} */ @Override public boolean canDeleteModel(final QName modelName) { boolean canDeleteModel = true; // TODO add model locking during delete (would need to be tenant-aware & cluster-aware) to avoid potential // for concurrent addition of new content/workflow as model is being deleted final Collection namespaceDefs; final Collection typeDefs; final Collection aspectDefs; try { namespaceDefs = dictionaryDAO.getNamespaces(modelName); typeDefs = dictionaryDAO.getTypes(modelName); aspectDefs = dictionaryDAO.getAspects(modelName); // TODO - in case of MT we do not currently allow deletion of an overridden model (with usages) ... but could allow if (re-)inherited model is equivalent to an incremental update only ? canDeleteModel &= canDeleteModel(namespaceDefs, typeDefs, aspectDefs, null); if(canDeleteModel) { if (tenantService.isEnabled() && tenantService.isTenantUser() == false) { // TODO should fix this up - won't scale // shared model - need to check all tenants (whether enabled or disabled) unless they have overridden List tenants = tenantAdminService.getAllTenants(); for (final Tenant tenant : tenants) { // validate model delete within context of tenant domain canDeleteModel &= AuthenticationUtil.runAs(new RunAsWork() { public Boolean doWork() { boolean canDelete = canDeleteModel(namespaceDefs, typeDefs, aspectDefs, tenant); return canDelete; } }, tenantService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenant.getTenantDomain())); if(!canDeleteModel) { break; } } } } } catch (DictionaryException e) { if (logger.isDebugEnabled()) { logger.debug("Dictionary model '" + modelName + "' does not exist ... skip delete validation : " + e); } // we must return true here - there is no model canDeleteModel = true; } return canDeleteModel; } /** * {@inheritDoc} */ @Override public void validateModel(CompiledModel compiledModel) { ModelDefinition modelDef = compiledModel.getModelDefinition(); QName modelName = modelDef.getName(); M2Model model = compiledModel.getM2Model(); checkCustomModelNamespace(model, TenantUtil.getCurrentDomain()); List modelDiffs = dictionaryDAO.diffModel(model); for (M2ModelDiff modelDiff : modelDiffs) { if (modelDiff.getDiffType().equals(M2ModelDiff.DIFF_DELETED)) { // TODO - check tenants if model is shared / inherited if (modelDiff.getElementType().equals(M2ModelDiff.TYPE_PROPERTY)) { validateDeleteProperty(modelName, modelDiff.getElementName(), false); } else if (modelDiff.getElementType().equals(M2ModelDiff.TYPE_CONSTRAINT)) { validateDeleteConstraint(compiledModel, modelDiff.getElementName(), false); } else { /* * As the M2Model#compile method will detect and throw exception for any missing namespace which * is required to define any Type, Aspect or Property, we can safely add this extra check. * See APPSREPO-59 comment for details. */ if (!modelDiff.getElementType().equals(M2ModelDiff.TYPE_NAMESPACE)) { throw new AlfrescoRuntimeException("Failed to validate model update - found deleted " + modelDiff.getElementType() + " '" + modelDiff .getElementName() + "'"); } } } if (modelDiff.getDiffType().equals(M2ModelDiff.DIFF_UPDATED)) { throw new AlfrescoRuntimeException("Failed to validate model update - found non-incrementally updated " + modelDiff.getElementType() + " '" + modelDiff.getElementName() + "'"); } if(modelDiff.getDiffType().equals(M2ModelDiff.DIFF_CREATED)) { if (modelDiff.getElementType().equals(M2ModelDiff.TYPE_NAMESPACE)) { ModelDefinition importedModel = dictionaryService.getModelByNamespaceUri(modelDiff.getNamespaceDefinition().getUri()); if(importedModel != null && !model.getNamespaces().isEmpty()) { checkCircularDependency(importedModel, model, importedModel.getName().getLocalName()); } } } } // TODO validate that any deleted constraints are not being referenced - else currently will become anon - or push down into model compilation (check backwards compatibility ...) } private void checkCircularDependency(ModelDefinition model, M2Model existingModel, String parentPrefixedName) throws AlfrescoRuntimeException { for (NamespaceDefinition importedNamespace : model.getImportedNamespaces()) { ModelDefinition md = null; if ((md = dictionaryService.getModelByNamespaceUri(importedNamespace.getUri())) != null) { if (existingModel.getNamespace(importedNamespace.getUri()) != null) { throw new AlfrescoRuntimeException("Failed to validate model update - found circular dependency. You can't set parent " + parentPrefixedName + " as it's model already depends on " + existingModel.getName()); } checkCircularDependency(md, existingModel, parentPrefixedName); } } } }