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.ArrayList; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031 032import org.apache.commons.codec.binary.Hex; 033import org.apache.commons.lang.StringUtils; 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.common.utils.IdUtils; 037import org.nuxeo.common.utils.Path; 038import org.nuxeo.ecm.core.api.CoreInstance; 039import org.nuxeo.ecm.core.api.CoreSession; 040import org.nuxeo.ecm.core.api.DocumentModel; 041import org.nuxeo.ecm.core.api.DocumentModelList; 042import org.nuxeo.ecm.core.api.DocumentNotFoundException; 043import org.nuxeo.ecm.core.api.DocumentSecurityException; 044import org.nuxeo.ecm.core.api.NuxeoException; 045import org.nuxeo.ecm.core.api.NuxeoPrincipal; 046import org.nuxeo.ecm.core.api.PathRef; 047import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 048import org.nuxeo.ecm.core.api.event.CoreEventConstants; 049import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 050import org.nuxeo.ecm.core.api.security.SecurityConstants; 051import org.nuxeo.ecm.core.event.Event; 052import org.nuxeo.ecm.core.event.EventContext; 053import org.nuxeo.ecm.core.event.EventProducer; 054import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 055import org.nuxeo.ecm.core.event.impl.EventContextImpl; 056import org.nuxeo.ecm.platform.usermanager.UserAdapter; 057import org.nuxeo.ecm.platform.usermanager.UserManager; 058import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService; 059import org.nuxeo.ecm.platform.userworkspace.constants.UserWorkspaceConstants; 060import org.nuxeo.runtime.api.Framework; 061 062/** 063 * Abstract class holding most of the logic for using {@link UnrestrictedSessionRunner} while creating UserWorkspaces 064 * and associated resources 065 * 066 * @author tiry 067 * @since 5.9.5 068 */ 069public abstract class AbstractUserWorkspaceImpl implements UserWorkspaceService { 070 071 private static final Log log = LogFactory.getLog(DefaultUserWorkspaceServiceImpl.class); 072 073 private static final long serialVersionUID = 1L; 074 075 protected static final char ESCAPE_CHAR = '~'; 076 077 protected static final String ESCAPED_CHARS = ESCAPE_CHAR + "/\\?&;@"; 078 079 protected String targetDomainName; 080 081 protected final int maxsize; 082 083 public AbstractUserWorkspaceImpl() { 084 super(); 085 maxsize = Framework.getService(PathSegmentService.class) 086 .getMaxSize(); 087 } 088 089 protected String getDomainName(CoreSession userCoreSession, DocumentModel currentDocument) { 090 if (targetDomainName == null) { 091 RootDomainFinder finder = new RootDomainFinder(userCoreSession); 092 finder.runUnrestricted(); 093 targetDomainName = finder.domaineName; 094 } 095 return targetDomainName; 096 } 097 098 /** 099 * Gets the base username to use to determine a user's workspace. This is not used directly as a path segment, but 100 * forms the sole basis for it. 101 * 102 * @since 9.2 103 */ 104 protected String getUserName(Principal principal, String username) { 105 if (principal instanceof NuxeoPrincipal) { 106 username = ((NuxeoPrincipal) principal).getActingUser(); 107 } else if (username == null) { 108 username = principal.getName(); 109 } 110 if (NuxeoPrincipal.isTransientUsername(username)) { 111 // no personal workspace for transient users 112 username = null; 113 } 114 if (StringUtils.isEmpty(username)) { 115 username = null; 116 } 117 return username; 118 } 119 120 /** 121 * Finds the list of potential names for the user workspace. They're all tried in order. 122 * 123 * @return the list of candidate names 124 * @since 9.2 125 */ 126 // public for tests 127 public List<String> getCandidateUserWorkspaceNames(String username) { 128 List<String> names = new ArrayList<>(); 129 names.add(escape(username)); 130 generateCandidates(names, username, maxsize); // compat 131 generateCandidates(names, username, 30); // old compat 132 return names; 133 } 134 135 /** 136 * Bijective escaping for user names. 137 * <p> 138 * Escapes some chars not allowed in a path segment or URL. The escaping character is a {@code ~} followed by the 139 * one-byte hex value of the character. 140 * 141 * @since 9.2 142 */ 143 protected String escape(String string) { 144 StringBuilder buf = new StringBuilder(string.length()); 145 for (char c : string.toCharArray()) { 146 if (ESCAPED_CHARS.indexOf(c) == -1) { 147 buf.append(c); 148 } else { 149 buf.append(ESCAPE_CHAR); 150 if (c < 16) { 151 buf.append('0'); 152 } 153 buf.append(Integer.toHexString(c)); // assumed to be < 256 154 } 155 } 156 // don't re-allocate a new string if we didn't escape anything 157 return buf.length() == string.length() ? string : buf.toString(); 158 } 159 160 protected void generateCandidates(List<String> names, String username, int max) { 161 String name = IdUtils.generateId(username, "-", false, max); 162 if (!names.contains(name)) { 163 names.add(name); 164 } 165 if (name.length() == max) { // at max size or truncated 166 String digested = name.substring(0, name.length() - 8) + digest(username, 8); 167 if (!names.contains(digested)) { 168 names.add(digested); 169 } 170 } 171 } 172 173 protected String digest(String username, int maxsize) { 174 try { 175 MessageDigest crypt = MessageDigest.getInstance("SHA-1"); 176 crypt.update(username.getBytes()); 177 return new String(Hex.encodeHex(crypt.digest())).substring(0, maxsize); 178 } catch (NoSuchAlgorithmException cause) { 179 throw new NuxeoException("Cannot digest " + username, cause); 180 } 181 } 182 183 protected String computePathUserWorkspaceRoot(CoreSession userCoreSession, String usedUsername, 184 DocumentModel currentDocument) { 185 String domainName = getDomainName(userCoreSession, currentDocument); 186 if (domainName == null) { 187 throw new NuxeoException("Unable to find root domain for UserWorkspace"); 188 } 189 return new Path("/" + domainName) 190 .append(UserWorkspaceConstants.USERS_PERSONAL_WORKSPACES_ROOT) 191 .toString(); 192 } 193 194 @Override 195 public DocumentModel getCurrentUserPersonalWorkspace(String userName, DocumentModel currentDocument) { 196 if (currentDocument == null) { 197 return null; 198 } 199 return getCurrentUserPersonalWorkspace(null, userName, currentDocument.getCoreSession(), currentDocument); 200 } 201 202 @Override 203 public DocumentModel getCurrentUserPersonalWorkspace(CoreSession userCoreSession, DocumentModel context) { 204 return getCurrentUserPersonalWorkspace(userCoreSession.getPrincipal(), null, userCoreSession, context); 205 } 206 207 /** 208 * This method handles the UserWorkspace creation with a Principal or a username. At least one should be passed. If 209 * a principal is passed, the username is not taken into account. 210 * 211 * @since 5.7 "userWorkspaceCreated" is triggered 212 */ 213 protected DocumentModel getCurrentUserPersonalWorkspace(Principal principal, String userName, 214 CoreSession userCoreSession, DocumentModel context) { 215 String usedUsername = getUserName(principal, userName); 216 if (usedUsername == null) { 217 return null; 218 } 219 PathRef rootref = getExistingUserWorkspaceRoot(userCoreSession, usedUsername, context); 220 PathRef uwref = getExistingUserWorkspace(userCoreSession, rootref, principal, usedUsername); 221 DocumentModel uw = userCoreSession.getDocument(uwref); 222 223 return uw; 224 } 225 226 protected PathRef getExistingUserWorkspaceRoot(CoreSession session, String username, DocumentModel context) { 227 PathRef rootref = new PathRef(computePathUserWorkspaceRoot(session, username, context)); 228 if (session.exists(rootref)) { 229 return rootref; 230 } 231 return new PathRef(new UnrestrictedRootCreator(rootref, username, session).create().getPathAsString()); 232 } 233 234 protected PathRef getExistingUserWorkspace(CoreSession session, PathRef rootref, Principal principal, 235 String username) { 236 PathRef freeRef = null; 237 for (String name : getCandidateUserWorkspaceNames(username)) { 238 PathRef ref = new PathRef(rootref, name); 239 if (session.exists(ref) 240 && session.hasPermission(session.getPrincipal(), ref, SecurityConstants.EVERYTHING)) { 241 return ref; 242 } 243 @SuppressWarnings("boxing") 244 boolean exists = CoreInstance.doPrivileged(session, (CoreSession s) -> s.exists(ref)); 245 if (!exists && freeRef == null) { 246 // we have a candidate name for creation if we don't find anything else 247 freeRef = ref; 248 } 249 // else if exists it means there's a collision with the truncated workspace of another user 250 // try next name 251 } 252 if (freeRef != null) { 253 PathRef ref = freeRef; // effectively final 254 CoreInstance.doPrivileged(session, s -> { 255 doCreateUserWorkspace(s, ref, principal, username); 256 }); 257 return freeRef; 258 } 259 // couldn't find anything, because we lacked permission to existing docs (collision) 260 throw new DocumentSecurityException(username); 261 } 262 263 @Override 264 public DocumentModel getUserPersonalWorkspace(NuxeoPrincipal principal, DocumentModel context) { 265 return getCurrentUserPersonalWorkspace(principal, null, context.getCoreSession(), context); 266 } 267 268 @Override 269 public DocumentModel getUserPersonalWorkspace(String userName, DocumentModel context) { 270 UnrestrictedUserWorkspaceFinder finder = new UnrestrictedUserWorkspaceFinder(userName, context); 271 finder.runUnrestricted(); 272 return finder.getDetachedUserWorkspace(); 273 } 274 275 @Override 276 public boolean isUnderUserWorkspace(Principal principal, String username, DocumentModel doc) { 277 if (doc == null) { 278 return false; 279 } 280 username = getUserName(principal, username); 281 if (username == null) { 282 return false; 283 } 284 285 // fast checks that are useful to return a negative without the cost of accessing the user workspace 286 Path path = doc.getPath(); 287 if (path.segmentCount() < 2) { 288 return false; 289 } 290 // check domain 291 String domainName = getDomainName(doc.getCoreSession(), doc); 292 if (!domainName.equals(path.segment(0))) { 293 return false; 294 } 295 // check UWS root 296 if (!UserWorkspaceConstants.USERS_PERSONAL_WORKSPACES_ROOT.equals(path.segment(1))) { 297 return false; 298 } 299 // check workspace name among candidates 300 if (!getCandidateUserWorkspaceNames(username).contains(path.segment(2))) { 301 return false; 302 } 303 304 // fetch actual workspace to compare its path 305 DocumentModel uws = getCurrentUserPersonalWorkspace(principal, username, doc.getCoreSession(), doc); 306 return uws.getPath().isPrefixOf(doc.getPath()); 307 } 308 309 protected String buildUserWorkspaceTitle(Principal principal, String userName) { 310 if (userName == null) {// avoid looking for UserManager for nothing 311 return null; 312 } 313 // get the user service 314 UserManager userManager = Framework.getService(UserManager.class); 315 if (userManager == null) { 316 // for tests 317 return userName; 318 } 319 320 // Adapter userModel to get its fields (firstname, lastname) 321 DocumentModel userModel = userManager.getUserModel(userName); 322 if (userModel == null) { 323 return userName; 324 } 325 326 UserAdapter userAdapter = null; 327 userAdapter = userModel.getAdapter(UserAdapter.class); 328 329 if (userAdapter == null) { 330 return userName; 331 } 332 333 // compute the title 334 StringBuilder title = new StringBuilder(); 335 String firstName = userAdapter.getFirstName(); 336 if (firstName != null && firstName.trim() 337 .length() > 0) { 338 title.append(firstName); 339 } 340 341 String lastName = userAdapter.getLastName(); 342 if (lastName != null && lastName.trim() 343 .length() > 0) { 344 if (title.length() > 0) { 345 title.append(" "); 346 } 347 title.append(lastName); 348 } 349 350 if (title.length() > 0) { 351 return title.toString(); 352 } 353 354 return userName; 355 356 } 357 358 protected void notifyEvent(CoreSession coreSession, DocumentModel document, NuxeoPrincipal principal, 359 String eventId, Map<String, Serializable> properties) { 360 if (properties == null) { 361 properties = new HashMap<String, Serializable>(); 362 } 363 EventContext eventContext = null; 364 if (document != null) { 365 properties.put(CoreEventConstants.REPOSITORY_NAME, document.getRepositoryName()); 366 properties.put(CoreEventConstants.SESSION_ID, coreSession.getSessionId()); 367 properties.put(CoreEventConstants.DOC_LIFE_CYCLE, document.getCurrentLifeCycleState()); 368 eventContext = new DocumentEventContext(coreSession, principal, document); 369 } else { 370 eventContext = new EventContextImpl(coreSession, principal); 371 } 372 eventContext.setProperties(properties); 373 Event event = eventContext.newEvent(eventId); 374 Framework.getLocalService(EventProducer.class) 375 .fireEvent(event); 376 } 377 378 protected class UnrestrictedRootCreator extends UnrestrictedSessionRunner { 379 public UnrestrictedRootCreator(PathRef ref, String username, CoreSession session) { 380 super(session); 381 this.ref = ref; 382 this.username = username; 383 } 384 PathRef ref; 385 final String username; 386 DocumentModel doc; 387 388 @Override 389 public void run() { 390 if (session.exists(ref)) { 391 doc = session.getDocument(ref); 392 } else { 393 try { 394 doc = doCreateUserWorkspacesRoot(session, ref); 395 } catch (DocumentNotFoundException e) { 396 // domain may have been removed ! 397 targetDomainName = null; 398 ref = new PathRef(computePathUserWorkspaceRoot(session, username, null)); 399 doc = doCreateUserWorkspacesRoot(session, ref); 400 } 401 } 402 doc.detach(true); 403 assert (doc.getPathAsString() 404 .equals(ref.toString())); 405 } 406 407 DocumentModel create() { 408 synchronized (UnrestrictedRootCreator.class) { 409 runUnrestricted(); 410 return doc; 411 } 412 } 413 } 414 415 protected class UnrestrictedUserWorkspaceFinder extends UnrestrictedSessionRunner { 416 417 protected DocumentModel userWorkspace; 418 419 protected String userName; 420 421 protected DocumentModel context; 422 423 protected UnrestrictedUserWorkspaceFinder(String userName, DocumentModel context) { 424 super(context.getCoreSession() 425 .getRepositoryName(), userName); 426 this.userName = userName; 427 this.context = context; 428 } 429 430 @Override 431 public void run() { 432 userWorkspace = getCurrentUserPersonalWorkspace(null, userName, session, context); 433 if (userWorkspace != null) { 434 userWorkspace.detach(true); 435 } 436 } 437 438 public DocumentModel getDetachedUserWorkspace() { 439 return userWorkspace; 440 } 441 } 442 443 protected class RootDomainFinder extends UnrestrictedSessionRunner { 444 445 public RootDomainFinder(CoreSession userCoreSession) { 446 super(userCoreSession); 447 } 448 449 protected String domaineName; 450 451 @Override 452 public void run() { 453 454 String targetName = getComponent().getTargetDomainName(); 455 PathRef ref = new PathRef("/" + targetName); 456 if (session.exists(ref)) { 457 domaineName = targetName; 458 return; 459 } 460 // configured domain does not exist !!! 461 DocumentModelList domains = session.query("select * from Domain order by dc:created"); 462 463 if (!domains.isEmpty()) { 464 domaineName = domains.get(0) 465 .getName(); 466 } 467 } 468 } 469 470 protected UserWorkspaceServiceImplComponent getComponent() { 471 return (UserWorkspaceServiceImplComponent) Framework.getRuntime() 472 .getComponent( 473 UserWorkspaceServiceImplComponent.NAME); 474 } 475 476 protected abstract DocumentModel doCreateUserWorkspacesRoot(CoreSession unrestrictedSession, PathRef rootRef); 477 478 protected abstract DocumentModel doCreateUserWorkspace(CoreSession unrestrictedSession, PathRef wsRef, 479 Principal principal, String userName); 480 481}