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}