001/* 002 * (C) Copyright 2006-2018 Nuxeo (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 <[email protected]> 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.lang3.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.UnrestrictedSessionRunner; 043import org.nuxeo.ecm.core.api.repository.RepositoryManager; 044import org.nuxeo.ecm.core.api.security.ACE; 045import org.nuxeo.ecm.core.api.security.ACL; 046import org.nuxeo.ecm.core.api.security.ACP; 047import org.nuxeo.ecm.core.api.trash.TrashService; 048import org.nuxeo.ecm.directory.Session; 049import org.nuxeo.ecm.directory.api.DirectoryService; 050import org.nuxeo.runtime.api.Framework; 051import org.nuxeo.runtime.model.ComponentContext; 052import org.nuxeo.runtime.model.ComponentInstance; 053import org.nuxeo.runtime.model.DefaultComponent; 054import org.nuxeo.runtime.transaction.TransactionHelper; 055 056/** 057 * @author <a href="mailto:[email protected]">Thomas Roger</a> 058 * @since 5.6 059 */ 060public class MultiTenantServiceImpl extends DefaultComponent implements MultiTenantService { 061 062 private static final Log log = LogFactory.getLog(MultiTenantServiceImpl.class); 063 064 public static final String CONFIGURATION_EP = "configuration"; 065 066 private MultiTenantConfiguration configuration; 067 068 private Boolean isTenantIsolationEnabled; 069 070 @Override 071 public boolean isTenantIsolationEnabledByDefault() { 072 return configuration.isEnabledByDefault(); 073 } 074 075 @Override 076 public String getTenantDocumentType() { 077 return configuration.getTenantDocumentType(); 078 } 079 080 @Override 081 public boolean isTenantIsolationEnabled(CoreSession session) { 082 if (isTenantIsolationEnabled == null) { 083 final List<DocumentModel> tenants = new ArrayList<>(); 084 new UnrestrictedSessionRunner(session) { 085 @Override 086 public void run() { 087 String query = "SELECT * FROM Document WHERE ecm:mixinType = 'TenantConfig' AND ecm:isTrashed = 0"; 088 tenants.addAll(session.query(query)); 089 } 090 }.runUnrestricted(); 091 isTenantIsolationEnabled = !tenants.isEmpty(); 092 } 093 return isTenantIsolationEnabled; 094 } 095 096 @Override 097 public void enableTenantIsolation(CoreSession session) { 098 if (!isTenantIsolationEnabled(session)) { 099 new UnrestrictedSessionRunner(session) { 100 @Override 101 public void run() { 102 String query = "SELECT * FROM Document WHERE ecm:primaryType = '%s' AND ecm:isTrashed = 0"; 103 List<DocumentModel> docs = session.query( 104 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:isTrashed = 0"; 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.getService(DirectoryService.class); 149 try (Session session = directoryService.open(TENANTS_DIRECTORY)) { 150 Map<String, Object> m = new HashMap<>(); 151 m.put("id", getTenantIdForTenant(doc)); 152 m.put("label", doc.getTitle()); 153 m.put("docId", doc.getId()); 154 return Framework.doPrivileged(() -> 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<>(); 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.getService(DirectoryService.class); 204 try (Session session = directoryService.open(TENANTS_DIRECTORY)) { 205 Framework.doPrivileged(() -> 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.isTrashed()) { 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.getService(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 TransactionHelper.runInTransaction(() -> { 246 RepositoryManager repositoryManager = Framework.getService(RepositoryManager.class); 247 for (String repositoryName : repositoryManager.getRepositoryNames()) { 248 new UnrestrictedSessionRunner(repositoryName) { 249 @Override 250 public void run() { 251 if (isTenantIsolationEnabledByDefault() && !isTenantIsolationEnabled(session)) { 252 enableTenantIsolation(session); 253 } 254 } 255 }.runUnrestricted(); 256 } 257 }); 258 } 259 260 @Override 261 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 262 if (CONFIGURATION_EP.equals(extensionPoint)) { 263 if (configuration != null) { 264 log.warn("Overriding existing multi tenant configuration"); 265 } 266 configuration = (MultiTenantConfiguration) contribution; 267 } 268 } 269 270 @Override 271 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 272 if (CONFIGURATION_EP.equals(extensionPoint)) { 273 if (contribution.equals(configuration)) { 274 configuration = null; 275 } 276 } 277 } 278 279 @Override 280 public List<String> getProhibitedGroups() { 281 if (configuration != null) { 282 return configuration.getProhibitedGroups(); 283 } 284 return null; 285 } 286}