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