/*
* (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*
* $Id$
*/
package org.nuxeo.ecm.platform.userworkspace.core.service;
import java.io.Serializable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.IdUtils;
import org.nuxeo.common.utils.Path;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.DocumentNotFoundException;
import org.nuxeo.ecm.core.api.DocumentSecurityException;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.PathRef;
import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
import org.nuxeo.ecm.core.api.event.CoreEventConstants;
import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventContext;
import org.nuxeo.ecm.core.event.EventProducer;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
import org.nuxeo.ecm.core.event.impl.EventContextImpl;
import org.nuxeo.ecm.platform.usermanager.UserAdapter;
import org.nuxeo.ecm.platform.usermanager.UserManager;
import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService;
import org.nuxeo.ecm.platform.userworkspace.constants.UserWorkspaceConstants;
import org.nuxeo.runtime.api.Framework;
/**
* Abstract class holding most of the logic for using {@link UnrestrictedSessionRunner} while creating UserWorkspaces
* and associated resources
*
* @author tiry
* @since 5.9.5
*/
public abstract class AbstractUserWorkspaceImpl implements UserWorkspaceService {
private static final Log log = LogFactory.getLog(DefaultUserWorkspaceServiceImpl.class);
private static final long serialVersionUID = 1L;
protected static final char ESCAPE_CHAR = '~';
protected static final String ESCAPED_CHARS = ESCAPE_CHAR + "/\\?&;@";
protected String targetDomainName;
protected final int maxsize;
public AbstractUserWorkspaceImpl() {
super();
maxsize = Framework.getService(PathSegmentService.class)
.getMaxSize();
}
protected String getDomainName(CoreSession userCoreSession, DocumentModel currentDocument) {
if (targetDomainName == null) {
RootDomainFinder finder = new RootDomainFinder(userCoreSession);
finder.runUnrestricted();
targetDomainName = finder.domaineName;
}
return targetDomainName;
}
/**
* Gets the base username to use to determine a user's workspace. This is not used directly as a path segment, but
* forms the sole basis for it.
*
* @since 9.2
*/
protected String getUserName(Principal principal, String username) {
if (principal instanceof NuxeoPrincipal) {
username = ((NuxeoPrincipal) principal).getActingUser();
} else if (username == null) {
username = principal.getName();
}
if (NuxeoPrincipal.isTransientUsername(username)) {
// no personal workspace for transient users
username = null;
}
if (StringUtils.isEmpty(username)) {
username = null;
}
return username;
}
/**
* Finds the list of potential names for the user workspace. They're all tried in order.
*
* @return the list of candidate names
* @since 9.2
*/
// public for tests
public List<String> getCandidateUserWorkspaceNames(String username) {
List<String> names = new ArrayList<>();
names.add(escape(username));
generateCandidates(names, username, maxsize); // compat
generateCandidates(names, username, 30); // old compat
return names;
}
/**
* Bijective escaping for user names.
* <p>
* Escapes some chars not allowed in a path segment or URL. The escaping character is a {@code ~} followed by the
* one-byte hex value of the character.
*
* @since 9.2
*/
protected String escape(String string) {
StringBuilder buf = new StringBuilder(string.length());
for (char c : string.toCharArray()) {
if (ESCAPED_CHARS.indexOf(c) == -1) {
buf.append(c);
} else {
buf.append(ESCAPE_CHAR);
if (c < 16) {
buf.append('0');
}
buf.append(Integer.toHexString(c)); // assumed to be < 256
}
}
// don't re-allocate a new string if we didn't escape anything
return buf.length() == string.length() ? string : buf.toString();
}
protected void generateCandidates(List<String> names, String username, int max) {
String name = IdUtils.generateId(username, "-", false, max);
if (!names.contains(name)) {
names.add(name);
}
if (name.length() == max) { // at max size or truncated
String digested = name.substring(0, name.length() - 8) + digest(username, 8);
if (!names.contains(digested)) {
names.add(digested);
}
}
}
protected String digest(String username, int maxsize) {
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.update(username.getBytes());
return new String(Hex.encodeHex(crypt.digest())).substring(0, maxsize);
} catch (NoSuchAlgorithmException cause) {
throw new NuxeoException("Cannot digest " + username, cause);
}
}
protected String computePathUserWorkspaceRoot(CoreSession userCoreSession, String usedUsername,
DocumentModel currentDocument) {
String domainName = getDomainName(userCoreSession, currentDocument);
if (domainName == null) {
throw new NuxeoException("Unable to find root domain for UserWorkspace");
}
return new Path("/" + domainName)
.append(UserWorkspaceConstants.USERS_PERSONAL_WORKSPACES_ROOT)
.toString();
}
@Override
public DocumentModel getCurrentUserPersonalWorkspace(String userName, DocumentModel currentDocument) {
if (currentDocument == null) {
return null;
}
return getCurrentUserPersonalWorkspace(null, userName, currentDocument.getCoreSession(), currentDocument);
}
@Override
public DocumentModel getCurrentUserPersonalWorkspace(CoreSession userCoreSession, DocumentModel context) {
return getCurrentUserPersonalWorkspace(userCoreSession.getPrincipal(), null, userCoreSession, context);
}
/**
* This method handles the UserWorkspace creation with a Principal or a username. At least one should be passed. If
* a principal is passed, the username is not taken into account.
*
* @since 5.7 "userWorkspaceCreated" is triggered
*/
protected DocumentModel getCurrentUserPersonalWorkspace(Principal principal, String userName,
CoreSession userCoreSession, DocumentModel context) {
String usedUsername = getUserName(principal, userName);
if (usedUsername == null) {
return null;
}
PathRef rootref = getExistingUserWorkspaceRoot(userCoreSession, usedUsername, context);
PathRef uwref = getExistingUserWorkspace(userCoreSession, rootref, principal, usedUsername);
DocumentModel uw = userCoreSession.getDocument(uwref);
return uw;
}
protected PathRef getExistingUserWorkspaceRoot(CoreSession session, String username, DocumentModel context) {
PathRef rootref = new PathRef(computePathUserWorkspaceRoot(session, username, context));
if (session.exists(rootref)) {
return rootref;
}
return new PathRef(new UnrestrictedRootCreator(rootref, username, session).create().getPathAsString());
}
protected PathRef getExistingUserWorkspace(CoreSession session, PathRef rootref, Principal principal,
String username) {
PathRef freeRef = null;
for (String name : getCandidateUserWorkspaceNames(username)) {
PathRef ref = new PathRef(rootref, name);
if (session.exists(ref)
&& session.hasPermission(session.getPrincipal(), ref, SecurityConstants.EVERYTHING)) {
return ref;
}
@SuppressWarnings("boxing")
boolean exists = CoreInstance.doPrivileged(session, (CoreSession s) -> s.exists(ref));
if (!exists && freeRef == null) {
// we have a candidate name for creation if we don't find anything else
freeRef = ref;
}
// else if exists it means there's a collision with the truncated workspace of another user
// try next name
}
if (freeRef != null) {
PathRef ref = freeRef; // effectively final
CoreInstance.doPrivileged(session, s -> {
doCreateUserWorkspace(s, ref, principal, username);
});
return freeRef;
}
// couldn't find anything, because we lacked permission to existing docs (collision)
throw new DocumentSecurityException(username);
}
@Override
public DocumentModel getUserPersonalWorkspace(NuxeoPrincipal principal, DocumentModel context) {
return getCurrentUserPersonalWorkspace(principal, null, context.getCoreSession(), context);
}
@Override
public DocumentModel getUserPersonalWorkspace(String userName, DocumentModel context) {
UnrestrictedUserWorkspaceFinder finder = new UnrestrictedUserWorkspaceFinder(userName, context);
finder.runUnrestricted();
return finder.getDetachedUserWorkspace();
}
@Override
public boolean isUnderUserWorkspace(Principal principal, String username, DocumentModel doc) {
if (doc == null) {
return false;
}
username = getUserName(principal, username);
if (username == null) {
return false;
}
// fast checks that are useful to return a negative without the cost of accessing the user workspace
Path path = doc.getPath();
if (path.segmentCount() < 2) {
return false;
}
// check domain
String domainName = getDomainName(doc.getCoreSession(), doc);
if (!domainName.equals(path.segment(0))) {
return false;
}
// check UWS root
if (!UserWorkspaceConstants.USERS_PERSONAL_WORKSPACES_ROOT.equals(path.segment(1))) {
return false;
}
// check workspace name among candidates
if (!getCandidateUserWorkspaceNames(username).contains(path.segment(2))) {
return false;
}
// fetch actual workspace to compare its path
DocumentModel uws = getCurrentUserPersonalWorkspace(principal, username, doc.getCoreSession(), doc);
return uws.getPath().isPrefixOf(doc.getPath());
}
protected String buildUserWorkspaceTitle(Principal principal, String userName) {
if (userName == null) {// avoid looking for UserManager for nothing
return null;
}
// get the user service
UserManager userManager = Framework.getService(UserManager.class);
if (userManager == null) {
// for tests
return userName;
}
// Adapter userModel to get its fields (firstname, lastname)
DocumentModel userModel = userManager.getUserModel(userName);
if (userModel == null) {
return userName;
}
UserAdapter userAdapter = null;
userAdapter = userModel.getAdapter(UserAdapter.class);
if (userAdapter == null) {
return userName;
}
// compute the title
StringBuilder title = new StringBuilder();
String firstName = userAdapter.getFirstName();
if (firstName != null && firstName.trim()
.length() > 0) {
title.append(firstName);
}
String lastName = userAdapter.getLastName();
if (lastName != null && lastName.trim()
.length() > 0) {
if (title.length() > 0) {
title.append(" ");
}
title.append(lastName);
}
if (title.length() > 0) {
return title.toString();
}
return userName;
}
protected void notifyEvent(CoreSession coreSession, DocumentModel document, NuxeoPrincipal principal,
String eventId, Map<String, Serializable> properties) {
if (properties == null) {
properties = new HashMap<String, Serializable>();
}
EventContext eventContext = null;
if (document != null) {
properties.put(CoreEventConstants.REPOSITORY_NAME, document.getRepositoryName());
properties.put(CoreEventConstants.SESSION_ID, coreSession.getSessionId());
properties.put(CoreEventConstants.DOC_LIFE_CYCLE, document.getCurrentLifeCycleState());
eventContext = new DocumentEventContext(coreSession, principal, document);
} else {
eventContext = new EventContextImpl(coreSession, principal);
}
eventContext.setProperties(properties);
Event event = eventContext.newEvent(eventId);
Framework.getLocalService(EventProducer.class)
.fireEvent(event);
}
protected class UnrestrictedRootCreator extends UnrestrictedSessionRunner {
public UnrestrictedRootCreator(PathRef ref, String username, CoreSession session) {
super(session);
this.ref = ref;
this.username = username;
}
PathRef ref;
final String username;
DocumentModel doc;
@Override
public void run() {
if (session.exists(ref)) {
doc = session.getDocument(ref);
} else {
try {
doc = doCreateUserWorkspacesRoot(session, ref);
} catch (DocumentNotFoundException e) {
// domain may have been removed !
targetDomainName = null;
ref = new PathRef(computePathUserWorkspaceRoot(session, username, null));
doc = doCreateUserWorkspacesRoot(session, ref);
}
}
doc.detach(true);
assert (doc.getPathAsString()
.equals(ref.toString()));
}
DocumentModel create() {
synchronized (UnrestrictedRootCreator.class) {
runUnrestricted();
return doc;
}
}
}
protected class UnrestrictedUserWorkspaceFinder extends UnrestrictedSessionRunner {
protected DocumentModel userWorkspace;
protected String userName;
protected DocumentModel context;
protected UnrestrictedUserWorkspaceFinder(String userName, DocumentModel context) {
super(context.getCoreSession()
.getRepositoryName(), userName);
this.userName = userName;
this.context = context;
}
@Override
public void run() {
userWorkspace = getCurrentUserPersonalWorkspace(null, userName, session, context);
if (userWorkspace != null) {
userWorkspace.detach(true);
}
}
public DocumentModel getDetachedUserWorkspace() {
return userWorkspace;
}
}
protected class RootDomainFinder extends UnrestrictedSessionRunner {
public RootDomainFinder(CoreSession userCoreSession) {
super(userCoreSession);
}
protected String domaineName;
@Override
public void run() {
String targetName = getComponent().getTargetDomainName();
PathRef ref = new PathRef("/" + targetName);
if (session.exists(ref)) {
domaineName = targetName;
return;
}
// configured domain does not exist !!!
DocumentModelList domains = session.query("select * from Domain order by dc:created");
if (!domains.isEmpty()) {
domaineName = domains.get(0)
.getName();
}
}
}
protected UserWorkspaceServiceImplComponent getComponent() {
return (UserWorkspaceServiceImplComponent) Framework.getRuntime()
.getComponent(
UserWorkspaceServiceImplComponent.NAME);
}
protected abstract DocumentModel doCreateUserWorkspacesRoot(CoreSession unrestrictedSession, PathRef rootRef);
protected abstract DocumentModel doCreateUserWorkspace(CoreSession unrestrictedSession, PathRef wsRef,
Principal principal, String userName);
}