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