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 <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.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:troger@nuxeo.com">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}