001/* 002 * (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Thomas Roger <troger@nuxeo.com> 016 */ 017 018package org.nuxeo.ecm.multi.tenant; 019 020import static org.nuxeo.ecm.core.api.security.SecurityConstants.EVERYONE; 021import static org.nuxeo.ecm.core.api.security.SecurityConstants.EVERYTHING; 022import static org.nuxeo.ecm.multi.tenant.Constants.POWER_USERS_GROUP; 023import static org.nuxeo.ecm.multi.tenant.Constants.TENANTS_DIRECTORY; 024import static org.nuxeo.ecm.multi.tenant.Constants.TENANT_CONFIG_FACET; 025import static org.nuxeo.ecm.multi.tenant.Constants.TENANT_ID_PROPERTY; 026import static org.nuxeo.ecm.multi.tenant.MultiTenantHelper.computeTenantAdministratorsGroup; 027import static org.nuxeo.ecm.multi.tenant.MultiTenantHelper.computeTenantMembersGroup; 028 029import java.security.Principal; 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034 035import org.apache.commons.lang.StringUtils; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.nuxeo.ecm.core.api.CoreSession; 039import org.nuxeo.ecm.core.api.DocumentModel; 040import org.nuxeo.ecm.core.api.LifeCycleConstants; 041import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 042import org.nuxeo.ecm.core.api.repository.RepositoryManager; 043import org.nuxeo.ecm.core.api.security.ACE; 044import org.nuxeo.ecm.core.api.security.ACL; 045import org.nuxeo.ecm.core.api.security.ACP; 046import org.nuxeo.ecm.core.trash.TrashService; 047import org.nuxeo.ecm.directory.Session; 048import org.nuxeo.ecm.directory.api.DirectoryService; 049import org.nuxeo.runtime.api.Framework; 050import org.nuxeo.runtime.model.ComponentContext; 051import org.nuxeo.runtime.model.ComponentInstance; 052import org.nuxeo.runtime.model.DefaultComponent; 053import org.nuxeo.runtime.transaction.TransactionHelper; 054 055/** 056 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a> 057 * @since 5.6 058 */ 059public class MultiTenantServiceImpl extends DefaultComponent implements MultiTenantService { 060 061 private static final Log log = LogFactory.getLog(MultiTenantServiceImpl.class); 062 063 public static final String CONFIGURATION_EP = "configuration"; 064 065 private MultiTenantConfiguration configuration; 066 067 private Boolean isTenantIsolationEnabled; 068 069 @Override 070 public boolean isTenantIsolationEnabledByDefault() { 071 return configuration.isEnabledByDefault(); 072 } 073 074 @Override 075 public String getTenantDocumentType() { 076 return configuration.getTenantDocumentType(); 077 } 078 079 @Override 080 public boolean isTenantIsolationEnabled(CoreSession session) { 081 if (isTenantIsolationEnabled == null) { 082 final List<DocumentModel> tenants = new ArrayList<DocumentModel>(); 083 new UnrestrictedSessionRunner(session) { 084 @Override 085 public void run() { 086 String query = "SELECT * FROM Document WHERE ecm:mixinType = 'TenantConfig' AND ecm:currentLifeCycleState != 'deleted'"; 087 tenants.addAll(session.query(query)); 088 } 089 }.runUnrestricted(); 090 isTenantIsolationEnabled = !tenants.isEmpty(); 091 } 092 return isTenantIsolationEnabled; 093 } 094 095 @Override 096 public void enableTenantIsolation(CoreSession session) { 097 if (!isTenantIsolationEnabled(session)) { 098 new UnrestrictedSessionRunner(session) { 099 @Override 100 public void run() { 101 String query = "SELECT * FROM Document WHERE ecm:primaryType = '%s' AND ecm:currentLifeCycleState != 'deleted'"; 102 List<DocumentModel> docs = session.query(String.format(query, configuration.getTenantDocumentType())); 103 for (DocumentModel doc : docs) { 104 enableTenantIsolationFor(session, doc); 105 } 106 session.save(); 107 } 108 }.runUnrestricted(); 109 isTenantIsolationEnabled = true; 110 } 111 } 112 113 @Override 114 public void disableTenantIsolation(CoreSession session) { 115 if (isTenantIsolationEnabled(session)) { 116 new UnrestrictedSessionRunner(session) { 117 @Override 118 public void run() { 119 String query = "SELECT * FROM Document WHERE ecm:mixinType = 'TenantConfig' AND ecm:currentLifeCycleState != 'deleted'"; 120 List<DocumentModel> docs = session.query(query); 121 for (DocumentModel doc : docs) { 122 disableTenantIsolationFor(session, doc); 123 } 124 session.save(); 125 } 126 }.runUnrestricted(); 127 isTenantIsolationEnabled = false; 128 } 129 } 130 131 @Override 132 public void enableTenantIsolationFor(CoreSession session, DocumentModel doc) { 133 if (!doc.hasFacet(TENANT_CONFIG_FACET)) { 134 doc.addFacet(TENANT_CONFIG_FACET); 135 } 136 137 DocumentModel d = registerTenant(doc); 138 String tenantId = (String) d.getPropertyValue("tenant:id"); 139 doc.setPropertyValue(TENANT_ID_PROPERTY, tenantId); 140 141 setTenantACL(tenantId, doc); 142 session.saveDocument(doc); 143 } 144 145 private DocumentModel registerTenant(DocumentModel doc) { 146 DirectoryService directoryService = Framework.getLocalService(DirectoryService.class); 147 try (Session session = directoryService.open(TENANTS_DIRECTORY)) { 148 Map<String, Object> m = new HashMap<String, Object>(); 149 m.put("id", getTenantIdForTenant(doc)); 150 m.put("label", doc.getTitle()); 151 m.put("docId", doc.getId()); 152 return session.createEntry(m); 153 } 154 } 155 156 private void setTenantACL(String tenantId, DocumentModel doc) { 157 ACP acp = doc.getACP(); 158 ACL acl = acp.getOrCreateACL(); 159 160 String tenantAdministratorsGroup = computeTenantAdministratorsGroup(tenantId); 161 acl.add(new ACE(tenantAdministratorsGroup, EVERYTHING, true)); 162 String tenantMembersGroup = computeTenantMembersGroup(tenantId); 163 String membersGroupPermission = configuration.getMembersGroupPermission(); 164 if (!StringUtils.isBlank(membersGroupPermission)) { 165 acl.add(new ACE(tenantMembersGroup, membersGroupPermission, true)); 166 } 167 acl.add(new ACE(EVERYONE, EVERYTHING, false)); 168 doc.setACP(acp, true); 169 } 170 171 @Override 172 public void disableTenantIsolationFor(CoreSession session, DocumentModel doc) { 173 if (session.exists(doc.getRef())) { 174 if (doc.hasFacet(TENANT_CONFIG_FACET)) { 175 doc.removeFacet(TENANT_CONFIG_FACET); 176 } 177 removeTenantACL(doc); 178 session.saveDocument(doc); 179 } 180 unregisterTenant(doc); 181 } 182 183 private void removeTenantACL(DocumentModel doc) { 184 ACP acp = doc.getACP(); 185 ACL acl = acp.getOrCreateACL(); 186 String tenantId = getTenantIdForTenant(doc); 187 188 // remove only the ACEs we added 189 String tenantAdministratorsGroup = computeTenantAdministratorsGroup(tenantId); 190 int tenantAdministratorsGroupACEIndex = acl.indexOf(new ACE(tenantAdministratorsGroup, EVERYTHING, true)); 191 if (tenantAdministratorsGroupACEIndex >= 0) { 192 List<ACE> newACEs = new ArrayList<ACE>(); 193 newACEs.addAll(acl.subList(0, tenantAdministratorsGroupACEIndex)); 194 newACEs.addAll(acl.subList(tenantAdministratorsGroupACEIndex + 3, acl.size())); 195 acl.setACEs(newACEs.toArray(new ACE[newACEs.size()])); 196 } 197 doc.setACP(acp, true); 198 } 199 200 private void unregisterTenant(DocumentModel doc) { 201 DirectoryService directoryService = Framework.getLocalService(DirectoryService.class); 202 try (Session session = directoryService.open(TENANTS_DIRECTORY)) { 203 session.deleteEntry(getTenantIdForTenant(doc)); 204 } 205 } 206 207 /** 208 * Gets the tenant id for a tenant document (Domain). 209 * <p> 210 * Deals with the case where it's a trashed document, which has a mangled name. 211 * 212 * @param doc the tenant document 213 * @return the tenant id 214 * @since 7.3 215 */ 216 protected String getTenantIdForTenant(DocumentModel doc) { 217 String name = doc.getName(); 218 if (doc.getCurrentLifeCycleState().equals(LifeCycleConstants.DELETED_STATE)) { 219 name = Framework.getService(TrashService.class).unmangleName(doc); 220 } 221 return name; 222 } 223 224 @Override 225 public List<DocumentModel> getTenants() { 226 DirectoryService directoryService = Framework.getLocalService(DirectoryService.class); 227 try (Session session = directoryService.open(TENANTS_DIRECTORY)) { 228 return session.getEntries(); 229 } 230 } 231 232 @Override 233 public boolean isTenantAdministrator(Principal principal) { 234 if (principal instanceof MultiTenantPrincipal) { 235 MultiTenantPrincipal p = (MultiTenantPrincipal) principal; 236 return p.getTenantId() != null && p.isMemberOf(POWER_USERS_GROUP); 237 } 238 return false; 239 } 240 241 @Override 242 public void applicationStarted(ComponentContext context) { 243 boolean started = false; 244 boolean ok = false; 245 try { 246 started = TransactionHelper.startTransaction(); 247 RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class); 248 for (String repositoryName : repositoryManager.getRepositoryNames()) { 249 new UnrestrictedSessionRunner(repositoryName) { 250 @Override 251 public void run() { 252 if (isTenantIsolationEnabledByDefault() && !isTenantIsolationEnabled(session)) { 253 enableTenantIsolation(session); 254 } 255 } 256 }.runUnrestricted(); 257 } 258 ok = true; 259 } finally { 260 if (started) { 261 try { 262 if (!ok) { 263 TransactionHelper.setTransactionRollbackOnly(); 264 } 265 } finally { 266 TransactionHelper.commitOrRollbackTransaction(); 267 } 268 } 269 } 270 } 271 272 @Override 273 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 274 if (CONFIGURATION_EP.equals(extensionPoint)) { 275 if (configuration != null) { 276 log.warn("Overriding existing multi tenant configuration"); 277 } 278 configuration = (MultiTenantConfiguration) contribution; 279 } 280 } 281 282 @Override 283 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 284 if (CONFIGURATION_EP.equals(extensionPoint)) { 285 if (contribution.equals(configuration)) { 286 configuration = null; 287 } 288 } 289 } 290 291 @Override 292 public List<String> getProhibitedGroups() { 293 if (configuration != null) { 294 return configuration.getProhibitedGroups(); 295 } 296 return null; 297 } 298}