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 * Bogdan Stefanescu 018 * George Lefter 019 * Stéfane Fermigier 020 * Julien Carsique 021 * Anahide Tchertchian 022 * Alexandre Russel 023 * Thierry Delprat 024 * Stéphane Lacoin 025 * Sun Seng David Tan 026 * Thomas Roger 027 * Thierry Martins 028 * Benoit Delbosc 029 * Florent Guillaume 030 */ 031package org.nuxeo.ecm.platform.usermanager; 032 033import java.io.ObjectStreamException; 034import java.io.Serializable; 035import java.security.Principal; 036import java.util.ArrayList; 037import java.util.HashSet; 038import java.util.LinkedList; 039import java.util.List; 040import java.util.Set; 041 042import org.apache.commons.logging.Log; 043import org.apache.commons.logging.LogFactory; 044import org.nuxeo.ecm.core.api.DataModel; 045import org.nuxeo.ecm.core.api.DocumentModel; 046import org.nuxeo.ecm.core.api.NuxeoException; 047import org.nuxeo.ecm.core.api.NuxeoGroup; 048import org.nuxeo.ecm.core.api.NuxeoPrincipal; 049import org.nuxeo.ecm.core.api.PropertyException; 050import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel; 051import org.nuxeo.ecm.core.api.security.SecurityConstants; 052import org.nuxeo.ecm.directory.DirectoryException; 053import org.nuxeo.runtime.api.Framework; 054 055public class NuxeoPrincipalImpl implements NuxeoPrincipal { 056 057 private static final long serialVersionUID = 1L; 058 059 private static final Log log = LogFactory.getLog(NuxeoPrincipalImpl.class); 060 061 protected UserConfig config = UserConfig.DEFAULT; 062 063 public final List<String> roles = new LinkedList<String>(); 064 065 // group not stored in the backend and added at login time 066 public List<String> virtualGroups = new LinkedList<String>(); 067 068 // transitive closure of the "member of group" relation 069 public List<String> allGroups; 070 071 public final boolean isAnonymous; 072 073 public boolean isAdministrator; 074 075 public String principalId; 076 077 public DocumentModel model; 078 079 public DataModel dataModel; 080 081 public String origUserName; 082 083 /** 084 * Constructor that sets principal to not anonymous, not administrator, and updates all the principal groups. 085 */ 086 public NuxeoPrincipalImpl(String name) { 087 this(name, false, false); 088 } 089 090 /** 091 * Constructor that sets principal to not administrator, and updates all the principal groups. 092 */ 093 public NuxeoPrincipalImpl(String name, boolean isAnonymous) { 094 this(name, isAnonymous, false); 095 } 096 097 /** 098 * Constructor that updates all the principal groups. 099 */ 100 public NuxeoPrincipalImpl(String name, boolean isAnonymous, boolean isAdministrator) { 101 this(name, isAnonymous, isAdministrator, true); 102 } 103 104 public NuxeoPrincipalImpl(String name, boolean isAnonymous, boolean isAdministrator, boolean updateAllGroups) { 105 DocumentModel documentModelImpl = new SimpleDocumentModel(config.schemaName); 106 // schema name hardcoded default when setModel is never called 107 // which happens when a principal is created just to encapsulate 108 // a username 109 setModel(documentModelImpl, updateAllGroups); 110 dataModel.setData(config.nameKey, name); 111 this.isAnonymous = isAnonymous; 112 this.isAdministrator = isAdministrator; 113 } 114 115 protected NuxeoPrincipalImpl(NuxeoPrincipalImpl other) { 116 config = other.config; 117 try { 118 model = other.model.clone(); 119 model.copyContextData(other.model); 120 } catch (CloneNotSupportedException cause) { 121 throw new NuxeoException("Cannot clone principal " + this); 122 } 123 dataModel = model.getDataModel(config.schemaName); 124 roles.addAll(other.roles); 125 allGroups = new ArrayList<>(other.allGroups); 126 virtualGroups = new ArrayList<>(other.virtualGroups); 127 isAdministrator = other.isAdministrator; 128 isAnonymous = other.isAnonymous; 129 origUserName = other.origUserName; 130 principalId = other.principalId; 131 } 132 133 public void setConfig(UserConfig config) { 134 this.config = config; 135 } 136 137 public UserConfig getConfig() { 138 return config; 139 } 140 141 @Override 142 public String getCompany() { 143 try { 144 return (String) dataModel.getData(config.companyKey); 145 } catch (PropertyException e) { 146 return null; 147 } 148 } 149 150 @Override 151 public void setCompany(String company) { 152 dataModel.setData(config.companyKey, company); 153 } 154 155 @Override 156 public String getFirstName() { 157 try { 158 return (String) dataModel.getData(config.firstNameKey); 159 } catch (PropertyException e) { 160 return null; 161 } 162 } 163 164 @Override 165 public void setFirstName(String firstName) { 166 dataModel.setData(config.firstNameKey, firstName); 167 } 168 169 @Override 170 public String getLastName() { 171 try { 172 return (String) dataModel.getData(config.lastNameKey); 173 } catch (PropertyException e) { 174 return null; 175 } 176 } 177 178 @Override 179 public void setLastName(String lastName) { 180 dataModel.setData(config.lastNameKey, lastName); 181 } 182 183 // impossible to modify the name - it is PK 184 @Override 185 public void setName(String name) { 186 dataModel.setData(config.nameKey, name); 187 } 188 189 @Override 190 public void setRoles(List<String> roles) { 191 this.roles.clear(); 192 this.roles.addAll(roles); 193 } 194 195 @Override 196 public void setGroups(List<String> groups) { 197 if (virtualGroups != null && !virtualGroups.isEmpty()) { 198 List<String> groupsToWrite = new ArrayList<String>(); 199 for (String group : groups) { 200 if (!virtualGroups.contains(group)) { 201 groupsToWrite.add(group); 202 } 203 } 204 dataModel.setData(config.groupsKey, groupsToWrite); 205 } else { 206 dataModel.setData(config.groupsKey, groups); 207 } 208 } 209 210 @Override 211 public String getName() { 212 try { 213 return (String) dataModel.getData(config.nameKey); 214 } catch (PropertyException e) { 215 return null; 216 } 217 } 218 219 @SuppressWarnings("unchecked") 220 @Override 221 public List<String> getGroups() { 222 List<String> groups = new LinkedList<String>(); 223 List<String> storedGroups; 224 try { 225 storedGroups = (List<String>) dataModel.getData(config.groupsKey); 226 } catch (PropertyException e) { 227 return null; 228 } 229 if (storedGroups != null) { 230 groups.addAll(storedGroups); 231 } 232 groups.addAll(virtualGroups); 233 return groups; 234 } 235 236 @Deprecated 237 @Override 238 public List<String> getRoles() { 239 return new ArrayList<String>(roles); 240 } 241 242 @Override 243 public void setPassword(String password) { 244 dataModel.setData(config.passwordKey, password); 245 } 246 247 @Override 248 public String getPassword() { 249 // password should never be read at the UI level for safety reasons 250 // + backend directories usually only store hashes that are useless 251 // except to check authentication at the directory level 252 return null; 253 } 254 255 @Override 256 public String toString() { 257 return (String) dataModel.getData(config.nameKey); 258 } 259 260 @Override 261 public String getPrincipalId() { 262 return principalId; 263 } 264 265 @Override 266 public void setPrincipalId(String principalId) { 267 this.principalId = principalId; 268 } 269 270 @Override 271 public String getEmail() { 272 try { 273 return (String) dataModel.getData(config.emailKey); 274 } catch (PropertyException e) { 275 return null; 276 } 277 } 278 279 @Override 280 public void setEmail(String email) { 281 dataModel.setData(config.emailKey, email); 282 } 283 284 @Override 285 public DocumentModel getModel() { 286 return model; 287 } 288 289 /** 290 * Sets model and recomputes all groups. 291 */ 292 public void setModel(DocumentModel model, boolean updateAllGroups) { 293 this.model = model; 294 dataModel = model.getDataModels().values().iterator().next(); 295 if (updateAllGroups) { 296 updateAllGroups(); 297 } 298 } 299 300 @Override 301 public void setModel(DocumentModel model) { 302 setModel(model, true); 303 } 304 305 @Override 306 public boolean isMemberOf(String group) { 307 return allGroups.contains(group); 308 } 309 310 @Override 311 public List<String> getAllGroups() { 312 return new ArrayList<String>(allGroups); 313 } 314 315 public void updateAllGroups() { 316 UserManager userManager = Framework.getService(UserManager.class); 317 Set<String> checkedGroups = new HashSet<String>(); 318 List<String> groupsToProcess = new ArrayList<String>(); 319 List<String> resultingGroups = new ArrayList<String>(); 320 groupsToProcess.addAll(getGroups()); 321 322 while (!groupsToProcess.isEmpty()) { 323 String groupName = groupsToProcess.remove(0); 324 if (!checkedGroups.contains(groupName)) { 325 checkedGroups.add(groupName); 326 NuxeoGroup nxGroup = null; 327 if (userManager != null) { 328 try { 329 nxGroup = userManager.getGroup(groupName); 330 } catch (DirectoryException de) { 331 if (virtualGroups.contains(groupName)) { 332 // do not fail while retrieving a virtual group 333 log.warn("Failed to get group '" + groupName + "' due to '" + de.getMessage() 334 + "': permission resolution involving groups may not be correct"); 335 nxGroup = null; 336 } else { 337 throw de; 338 } 339 } 340 } 341 if (nxGroup == null) { 342 if (virtualGroups.contains(groupName)) { 343 // just add the virtual group as is 344 resultingGroups.add(groupName); 345 } else if (userManager != null) { 346 // XXX this should only happens in case of 347 // inconsistency in DB 348 log.error("User " + getName() + " references the " + groupName + " group that does not exists"); 349 } 350 } else { 351 groupsToProcess.addAll(nxGroup.getParentGroups()); 352 // fetch the group name from the returned entry in case 353 // it does not have the same case than the actual entry in 354 // directory (for case insensitive directories) 355 resultingGroups.add(nxGroup.getName()); 356 // XXX: maybe remove group from virtual groups if it 357 // actually exists? otherwise it would be ignored when 358 // setting groups 359 } 360 } 361 } 362 363 allGroups = new ArrayList<String>(resultingGroups); 364 365 // set isAdministrator boolean according to groups declared on user 366 // manager 367 if (!isAdministrator() && userManager != null) { 368 List<String> adminGroups = userManager.getAdministratorsGroups(); 369 for (String adminGroup : adminGroups) { 370 if (allGroups.contains(adminGroup)) { 371 isAdministrator = true; 372 break; 373 } 374 } 375 } 376 } 377 378 public List<String> getVirtualGroups() { 379 return new ArrayList<String>(virtualGroups); 380 } 381 382 public void setVirtualGroups(List<String> virtualGroups, boolean updateAllGroups) { 383 this.virtualGroups = new ArrayList<String>(virtualGroups); 384 if (updateAllGroups) { 385 updateAllGroups(); 386 } 387 } 388 389 /** 390 * Sets virtual groups and recomputes all groups. 391 */ 392 public void setVirtualGroups(List<String> virtualGroups) { 393 setVirtualGroups(virtualGroups, true); 394 } 395 396 @Override 397 public boolean isAdministrator() { 398 return isAdministrator || SecurityConstants.SYSTEM_USERNAME.equals(getName()); 399 } 400 401 @Override 402 public String getTenantId() { 403 return null; 404 } 405 406 @Override 407 public boolean isAnonymous() { 408 return isAnonymous; 409 } 410 411 @Override 412 public boolean equals(Object other) { 413 if (other instanceof Principal) { 414 String name = getName(); 415 String otherName = ((Principal) other).getName(); 416 if (name == null) { 417 return otherName == null; 418 } else { 419 return name.equals(otherName); 420 } 421 } else { 422 return false; 423 } 424 } 425 426 @Override 427 public int hashCode() { 428 String name = getName(); 429 return name == null ? 0 : name.hashCode(); 430 } 431 432 @Override 433 public String getOriginatingUser() { 434 return origUserName; 435 } 436 437 @Override 438 public void setOriginatingUser(String originatingUser) { 439 origUserName = originatingUser; 440 } 441 442 @Override 443 public String getActingUser() { 444 return getOriginatingUser() == null ? getName() : getOriginatingUser(); 445 } 446 447 @Override 448 public boolean isTransient() { 449 String name = getName(); 450 return name != null && name.startsWith(TRANSIENT_USER_PREFIX); 451 } 452 453 protected NuxeoPrincipal cloneTransferable() { 454 return new TransferableClone(this); 455 } 456 457 /** 458 * Provides another implementation which marshall the user id instead of transferring the whole content and resolve 459 * it when unmarshalled. 460 */ 461 static protected class TransferableClone extends NuxeoPrincipalImpl { 462 463 protected TransferableClone(NuxeoPrincipalImpl other) { 464 super(other); 465 } 466 467 static class DataTransferObject implements Serializable { 468 469 private static final long serialVersionUID = 1L; 470 471 final String username; 472 473 final String originatingUser; 474 475 DataTransferObject(NuxeoPrincipal principal) { 476 username = principal.getName(); 477 originatingUser = principal.getOriginatingUser(); 478 } 479 480 private Object readResolve() throws ObjectStreamException { 481 UserManager userManager = Framework.getService(UserManager.class); 482 // look up principal as system user to avoid permission checks in directories 483 NuxeoPrincipal principal = Framework.doPrivileged(() -> userManager.getPrincipal(username)); 484 if (principal == null) { 485 throw new NullPointerException("No principal: " + username); 486 } 487 principal.setOriginatingUser(originatingUser); 488 return principal; 489 } 490 491 } 492 493 private Object writeReplace() throws ObjectStreamException { 494 return new DataTransferObject(this); 495 } 496 } 497}