001/*
002 * (C) Copyright 2006-2014 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 *     Nuxeo - initial API and implementation
018 *
019 * $Id$
020 */
021package org.nuxeo.ecm.platform.userworkspace.core.service;
022
023import java.io.Serializable;
024import java.security.MessageDigest;
025import java.security.NoSuchAlgorithmException;
026import java.security.Principal;
027import java.util.HashMap;
028import java.util.Map;
029
030import org.apache.commons.codec.binary.Hex;
031import org.apache.commons.lang.StringUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.common.utils.IdUtils;
035import org.nuxeo.common.utils.Path;
036import org.nuxeo.ecm.core.api.CoreSession;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.DocumentModelList;
039import org.nuxeo.ecm.core.api.DocumentNotFoundException;
040import org.nuxeo.ecm.core.api.NuxeoException;
041import org.nuxeo.ecm.core.api.NuxeoPrincipal;
042import org.nuxeo.ecm.core.api.PathRef;
043import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
044import org.nuxeo.ecm.core.api.event.CoreEventConstants;
045import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
046import org.nuxeo.ecm.core.api.security.SecurityConstants;
047import org.nuxeo.ecm.core.event.Event;
048import org.nuxeo.ecm.core.event.EventContext;
049import org.nuxeo.ecm.core.event.EventProducer;
050import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
051import org.nuxeo.ecm.core.event.impl.EventContextImpl;
052import org.nuxeo.ecm.platform.usermanager.UserAdapter;
053import org.nuxeo.ecm.platform.usermanager.UserManager;
054import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService;
055import org.nuxeo.ecm.platform.userworkspace.constants.UserWorkspaceConstants;
056import org.nuxeo.runtime.api.Framework;
057
058/**
059 * Abstract class holding most of the logic for using {@link UnrestrictedSessionRunner} while creating UserWorkspaces
060 * and associated resources
061 *
062 * @author tiry
063 * @since 5.9.5
064 */
065public abstract class AbstractUserWorkspaceImpl implements UserWorkspaceService {
066
067    private static final Log log = LogFactory.getLog(DefaultUserWorkspaceServiceImpl.class);
068
069    private static final long serialVersionUID = 1L;
070
071    protected String targetDomainName;
072
073    protected final int maxsize;
074
075    public AbstractUserWorkspaceImpl() {
076        super();
077        maxsize = Framework.getService(PathSegmentService.class)
078                .getMaxSize();
079    }
080
081    protected String getDomainName(CoreSession userCoreSession, DocumentModel currentDocument) {
082        if (targetDomainName == null) {
083            RootDomainFinder finder = new RootDomainFinder(userCoreSession);
084            finder.runUnrestricted();
085            targetDomainName = finder.domaineName;
086        }
087        return targetDomainName;
088    }
089
090    protected String getUserWorkspaceNameForUser(String username) {
091        return IdUtils.generateId(username, "-", false, maxsize);
092    }
093
094    protected String digest(String username, int maxsize) {
095        try {
096            MessageDigest crypt = MessageDigest.getInstance("SHA-1");
097            crypt.update(username.getBytes());
098            return new String(Hex.encodeHex(crypt.digest())).substring(0, maxsize);
099        } catch (NoSuchAlgorithmException cause) {
100            throw new NuxeoException("Cannot digest " + username, cause);
101        }
102    }
103
104    protected String computePathUserWorkspaceRoot(CoreSession userCoreSession, String usedUsername,
105            DocumentModel currentDocument) {
106        String domainName = getDomainName(userCoreSession, currentDocument);
107        if (domainName == null) {
108            throw new NuxeoException("Unable to find root domain for UserWorkspace");
109        }
110        return new Path("/" + domainName)
111                .append(UserWorkspaceConstants.USERS_PERSONAL_WORKSPACES_ROOT)
112                .toString();
113    }
114
115    @Override
116    public DocumentModel getCurrentUserPersonalWorkspace(String userName, DocumentModel currentDocument) {
117        if (currentDocument == null) {
118            return null;
119        }
120        return getCurrentUserPersonalWorkspace(null, userName, currentDocument.getCoreSession(), currentDocument);
121    }
122
123    @Override
124    public DocumentModel getCurrentUserPersonalWorkspace(CoreSession userCoreSession, DocumentModel context) {
125        return getCurrentUserPersonalWorkspace(userCoreSession.getPrincipal(), null, userCoreSession, context);
126    }
127
128    /**
129     * This method handles the UserWorkspace creation with a Principal or a username. At least one should be passed. If
130     * a principal is passed, the username is not taken into account.
131     *
132     * @since 5.7 "userWorkspaceCreated" is triggered
133     */
134    protected DocumentModel getCurrentUserPersonalWorkspace(Principal principal, String userName,
135            CoreSession userCoreSession, DocumentModel context) {
136        if (principal == null && StringUtils.isEmpty(userName)) {
137            return null;
138        }
139
140        String usedUsername;
141        if (principal instanceof NuxeoPrincipal) {
142            usedUsername = ((NuxeoPrincipal) principal).getActingUser();
143        } else {
144            usedUsername = userName;
145        }
146        if (NuxeoPrincipal.isTransientUsername(usedUsername)) {
147            // no personal workspace for transient users
148            return null;
149        }
150
151        PathRef rootref = getExistingUserWorkspaceRoot(userCoreSession, usedUsername, context);
152        PathRef uwref = getExistingUserWorkspace(userCoreSession, rootref, principal, usedUsername);
153        DocumentModel uw = userCoreSession.getDocument(uwref);
154
155        return uw;
156    }
157
158    protected PathRef getExistingUserWorkspaceRoot(CoreSession session, String username, DocumentModel context) {
159        PathRef rootref = new PathRef(computePathUserWorkspaceRoot(session, username, context));
160        if (session.exists(rootref)) {
161            return rootref;
162        }
163        return new PathRef(new UnrestrictedRootCreator(rootref, username, session).create().getPathAsString());
164    }
165
166    protected PathRef getExistingUserWorkspace(CoreSession session, PathRef rootref, Principal principal, String username) {
167        String workspacename = getUserWorkspaceNameForUser(username);
168        PathRef uwref = resolveUserWorkspace(session, rootref, username, workspacename, maxsize);
169        if (session.exists(uwref)) {
170            return uwref;
171        }
172        PathRef uwcompatref = resolveUserWorkspace(session, rootref, username, IdUtils.generateId(username, "-", false, 30), 30);
173        if (uwcompatref != null && session.exists(uwcompatref)) {
174            return uwcompatref;
175        }
176        new UnrestrictedUWSCreator(uwref, session, principal, username).create();
177        return uwref;
178    }
179
180    protected PathRef resolveUserWorkspace(CoreSession session, PathRef rootref, String username, String workspacename, int maxsize) {
181        PathRef uwref = new PathRef(rootref, workspacename);
182        if (workspacename.length() == maxsize && !new UnrestrictedPermissionChecker(session, uwref).hasPermission()) {
183            String substring = workspacename.substring(0, workspacename.length()-8);
184            return new PathRef(rootref, substring.concat(digest(username, 8)));
185        }
186        return uwref;
187    }
188
189    @Override
190    public DocumentModel getUserPersonalWorkspace(NuxeoPrincipal principal, DocumentModel context) {
191        return getCurrentUserPersonalWorkspace(principal, null, context.getCoreSession(), context);
192    }
193
194    @Override
195    public DocumentModel getUserPersonalWorkspace(String userName, DocumentModel context) {
196        UnrestrictedUserWorkspaceFinder finder = new UnrestrictedUserWorkspaceFinder(userName, context);
197        finder.runUnrestricted();
198        return finder.getDetachedUserWorkspace();
199    }
200
201    protected String buildUserWorkspaceTitle(Principal principal, String userName) {
202        if (userName == null) {// avoid looking for UserManager for nothing
203            return null;
204        }
205        // get the user service
206        UserManager userManager = Framework.getService(UserManager.class);
207        if (userManager == null) {
208            // for tests
209            return userName;
210        }
211
212        // Adapter userModel to get its fields (firstname, lastname)
213        DocumentModel userModel = userManager.getUserModel(userName);
214        if (userModel == null) {
215            return userName;
216        }
217
218        UserAdapter userAdapter = null;
219        userAdapter = userModel.getAdapter(UserAdapter.class);
220
221        if (userAdapter == null) {
222            return userName;
223        }
224
225        // compute the title
226        StringBuilder title = new StringBuilder();
227        String firstName = userAdapter.getFirstName();
228        if (firstName != null && firstName.trim()
229                .length() > 0) {
230            title.append(firstName);
231        }
232
233        String lastName = userAdapter.getLastName();
234        if (lastName != null && lastName.trim()
235                .length() > 0) {
236            if (title.length() > 0) {
237                title.append(" ");
238            }
239            title.append(lastName);
240        }
241
242        if (title.length() > 0) {
243            return title.toString();
244        }
245
246        return userName;
247
248    }
249
250    protected void notifyEvent(CoreSession coreSession, DocumentModel document, NuxeoPrincipal principal,
251            String eventId, Map<String, Serializable> properties) {
252        if (properties == null) {
253            properties = new HashMap<String, Serializable>();
254        }
255        EventContext eventContext = null;
256        if (document != null) {
257            properties.put(CoreEventConstants.REPOSITORY_NAME, document.getRepositoryName());
258            properties.put(CoreEventConstants.SESSION_ID, coreSession.getSessionId());
259            properties.put(CoreEventConstants.DOC_LIFE_CYCLE, document.getCurrentLifeCycleState());
260            eventContext = new DocumentEventContext(coreSession, principal, document);
261        } else {
262            eventContext = new EventContextImpl(coreSession, principal);
263        }
264        eventContext.setProperties(properties);
265        Event event = eventContext.newEvent(eventId);
266        Framework.getLocalService(EventProducer.class)
267                .fireEvent(event);
268    }
269
270    protected class UnrestrictedRootCreator extends UnrestrictedSessionRunner {
271        public UnrestrictedRootCreator(PathRef ref, String username, CoreSession session) {
272            super(session);
273            this.ref = ref;
274            this.username = username;
275        }
276        PathRef ref;
277        final String username;
278        DocumentModel doc;
279
280        @Override
281        public void run() {
282            if (session.exists(ref)) {
283                doc = session.getDocument(ref);
284            } else {
285                try {
286                    doc = doCreateUserWorkspacesRoot(session, ref);
287                } catch (DocumentNotFoundException e) {
288                    // domain may have been removed !
289                    targetDomainName = null;
290                    ref = new PathRef(computePathUserWorkspaceRoot(session, username, null));
291                    doc = doCreateUserWorkspacesRoot(session, ref);
292                }
293            }
294            doc.detach(true);
295            assert (doc.getPathAsString()
296                    .equals(ref.toString()));
297        }
298
299        DocumentModel create() {
300            synchronized (UnrestrictedRootCreator.class) {
301                runUnrestricted();
302                return doc;
303            }
304        }
305    }
306
307    protected class UnrestrictedUWSCreator extends UnrestrictedSessionRunner {
308
309        PathRef userWSRef;
310
311        String userName;
312
313        Principal principal;
314
315        DocumentModel uw;
316
317        public UnrestrictedUWSCreator(PathRef userWSRef, CoreSession userCoreSession,
318                Principal principal, String userName) {
319            super(userCoreSession);
320            this.userWSRef = userWSRef;
321            this.userName = userName;
322            this.principal = principal;
323        }
324
325        @Override
326        public void run() {
327            if (session.exists(userWSRef)) {
328                uw = session.getDocument(userWSRef);
329            } else {
330                uw = doCreateUserWorkspace(session, userWSRef, principal, userName);
331            }
332            uw.detach(true);
333            assert (uw.getPathAsString()
334                    .equals(userWSRef.toString()));
335        }
336
337        DocumentModel create() {
338            synchronized (UnrestrictedSessionRunner.class) {
339                runUnrestricted();
340                return uw;
341            }
342        }
343
344    }
345
346    protected class UnrestrictedPermissionChecker extends UnrestrictedSessionRunner {
347        protected UnrestrictedPermissionChecker(CoreSession session, PathRef ref) {
348            super(session);
349            this.ref = ref;
350            principal = session.getPrincipal();
351        }
352
353        final Principal principal;
354
355        final PathRef ref;
356
357        boolean hasPermission;
358
359        @Override
360        public void run() {
361            hasPermission = !session.exists(ref) || session.hasPermission(principal, ref, SecurityConstants.EVERYTHING);
362        }
363
364        boolean hasPermission() {
365            runUnrestricted();
366            return hasPermission;
367        }
368    }
369
370    protected class UnrestrictedUserWorkspaceFinder extends UnrestrictedSessionRunner {
371
372        protected DocumentModel userWorkspace;
373
374        protected String userName;
375
376        protected DocumentModel context;
377
378        protected UnrestrictedUserWorkspaceFinder(String userName, DocumentModel context) {
379            super(context.getCoreSession()
380                    .getRepositoryName(), userName);
381            this.userName = userName;
382            this.context = context;
383        }
384
385        @Override
386        public void run() {
387            userWorkspace = getCurrentUserPersonalWorkspace(null, userName, session, context);
388            if (userWorkspace != null) {
389                userWorkspace.detach(true);
390            }
391        }
392
393        public DocumentModel getDetachedUserWorkspace() {
394            return userWorkspace;
395        }
396    }
397
398    protected class RootDomainFinder extends UnrestrictedSessionRunner {
399
400        public RootDomainFinder(CoreSession userCoreSession) {
401            super(userCoreSession);
402        }
403
404        protected String domaineName;
405
406        @Override
407        public void run() {
408
409            String targetName = getComponent().getTargetDomainName();
410            PathRef ref = new PathRef("/" + targetName);
411            if (session.exists(ref)) {
412                domaineName = targetName;
413                return;
414            }
415            // configured domain does not exist !!!
416            DocumentModelList domains = session.query("select * from Domain order by dc:created");
417
418            if (!domains.isEmpty()) {
419                domaineName = domains.get(0)
420                        .getName();
421            }
422        }
423    }
424
425    protected UserWorkspaceServiceImplComponent getComponent() {
426        return (UserWorkspaceServiceImplComponent) Framework.getRuntime()
427                .getComponent(
428                        UserWorkspaceServiceImplComponent.NAME);
429    }
430
431    protected abstract DocumentModel doCreateUserWorkspacesRoot(CoreSession unrestrictedSession, PathRef rootRef);
432
433    protected abstract DocumentModel doCreateUserWorkspace(CoreSession unrestrictedSession, PathRef wsRef,
434            Principal principal, String userName);
435
436}