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}