/*
* (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:
* Bogdan Stefanescu
* George Lefter
* Stéfane Fermigier
* Julien Carsique
* Anahide Tchertchian
* Alexandre Russel
* Thierry Delprat
* Stéphane Lacoin
* Sun Seng David Tan
* Thomas Roger
* Thierry Martins
* Benoit Delbosc
* Florent Guillaume
*/
package org.nuxeo.ecm.platform.usermanager;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DataModel;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoGroup;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.core.api.impl.DataModelImpl;
import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.runtime.api.Framework;
public class NuxeoPrincipalImpl implements NuxeoPrincipal {
private static final long serialVersionUID = 1L;
private static final Log log = LogFactory.getLog(NuxeoPrincipalImpl.class);
protected UserConfig config = UserConfig.DEFAULT;
public final List<String> roles = new LinkedList<String>();
// group not stored in the backend and added at login time
public List<String> virtualGroups = new LinkedList<String>();
// transitive closure of the "member of group" relation
public List<String> allGroups;
public final boolean isAnonymous;
public boolean isAdministrator;
public String principalId;
public DocumentModel model;
public DataModel dataModel;
public String origUserName;
/**
* Constructor that sets principal to not anonymous, not administrator, and updates all the principal groups.
*/
public NuxeoPrincipalImpl(String name) {
this(name, false, false);
}
/**
* Constructor that sets principal to not administrator, and updates all the principal groups.
*/
public NuxeoPrincipalImpl(String name, boolean isAnonymous) {
this(name, isAnonymous, false);
}
/**
* Constructor that updates all the principal groups.
*/
public NuxeoPrincipalImpl(String name, boolean isAnonymous, boolean isAdministrator) {
this(name, isAnonymous, isAdministrator, true);
}
public NuxeoPrincipalImpl(String name, boolean isAnonymous, boolean isAdministrator, boolean updateAllGroups) {
DocumentModelImpl documentModelImpl = new DocumentModelImpl(config.schemaName);
// schema name hardcoded default when setModel is never called
// which happens when a principal is created just to encapsulate
// a username
documentModelImpl.addDataModel(new DataModelImpl(config.schemaName, new HashMap<String, Object>()));
setModel(documentModelImpl, updateAllGroups);
dataModel.setData(config.nameKey, name);
this.isAnonymous = isAnonymous;
this.isAdministrator = isAdministrator;
}
protected NuxeoPrincipalImpl(NuxeoPrincipalImpl other) {
config = other.config;
try {
model = other.model.clone();
} catch (CloneNotSupportedException cause) {
throw new NuxeoException("Cannot clone principal " + this);
}
dataModel = model.getDataModel(config.schemaName);
roles.addAll(other.roles);
allGroups = new ArrayList<>(other.allGroups);
virtualGroups = new ArrayList<>(other.virtualGroups);
isAdministrator = other.isAdministrator;
isAnonymous = other.isAnonymous;
origUserName = other.origUserName;
principalId = other.principalId;
}
public void setConfig(UserConfig config) {
this.config = config;
}
public UserConfig getConfig() {
return config;
}
@Override
public String getCompany() {
try {
return (String) dataModel.getData(config.companyKey);
} catch (PropertyException e) {
return null;
}
}
@Override
public void setCompany(String company) {
dataModel.setData(config.companyKey, company);
}
@Override
public String getFirstName() {
try {
return (String) dataModel.getData(config.firstNameKey);
} catch (PropertyException e) {
return null;
}
}
@Override
public void setFirstName(String firstName) {
dataModel.setData(config.firstNameKey, firstName);
}
@Override
public String getLastName() {
try {
return (String) dataModel.getData(config.lastNameKey);
} catch (PropertyException e) {
return null;
}
}
@Override
public void setLastName(String lastName) {
dataModel.setData(config.lastNameKey, lastName);
}
// impossible to modify the name - it is PK
@Override
public void setName(String name) {
dataModel.setData(config.nameKey, name);
}
@Override
public void setRoles(List<String> roles) {
this.roles.clear();
this.roles.addAll(roles);
}
@Override
public void setGroups(List<String> groups) {
if (virtualGroups != null && !virtualGroups.isEmpty()) {
List<String> groupsToWrite = new ArrayList<String>();
for (String group : groups) {
if (!virtualGroups.contains(group)) {
groupsToWrite.add(group);
}
}
dataModel.setData(config.groupsKey, groupsToWrite);
} else {
dataModel.setData(config.groupsKey, groups);
}
}
@Override
public String getName() {
try {
return (String) dataModel.getData(config.nameKey);
} catch (PropertyException e) {
return null;
}
}
@SuppressWarnings("unchecked")
@Override
public List<String> getGroups() {
List<String> groups = new LinkedList<String>();
List<String> storedGroups;
try {
storedGroups = (List<String>) dataModel.getData(config.groupsKey);
} catch (PropertyException e) {
return null;
}
if (storedGroups != null) {
groups.addAll(storedGroups);
}
groups.addAll(virtualGroups);
return groups;
}
@Deprecated
@Override
public List<String> getRoles() {
return new ArrayList<String>(roles);
}
@Override
public void setPassword(String password) {
dataModel.setData(config.passwordKey, password);
}
@Override
public String getPassword() {
// password should never be read at the UI level for safety reasons
// + backend directories usually only store hashes that are useless
// except to check authentication at the directory level
return null;
}
@Override
public String toString() {
return (String) dataModel.getData(config.nameKey);
}
@Override
public String getPrincipalId() {
return principalId;
}
@Override
public void setPrincipalId(String principalId) {
this.principalId = principalId;
}
@Override
public String getEmail() {
try {
return (String) dataModel.getData(config.emailKey);
} catch (PropertyException e) {
return null;
}
}
@Override
public void setEmail(String email) {
dataModel.setData(config.emailKey, email);
}
@Override
public DocumentModel getModel() {
return model;
}
/**
* Sets model and recomputes all groups.
*/
public void setModel(DocumentModel model, boolean updateAllGroups) {
this.model = model;
dataModel = model.getDataModels()
.values()
.iterator()
.next();
if (updateAllGroups) {
updateAllGroups();
}
}
@Override
public void setModel(DocumentModel model) {
setModel(model, true);
}
@Override
public boolean isMemberOf(String group) {
return allGroups.contains(group);
}
@Override
public List<String> getAllGroups() {
return new ArrayList<String>(allGroups);
}
public void updateAllGroups() {
UserManager userManager = Framework.getService(UserManager.class);
Set<String> checkedGroups = new HashSet<String>();
List<String> groupsToProcess = new ArrayList<String>();
List<String> resultingGroups = new ArrayList<String>();
groupsToProcess.addAll(getGroups());
while (!groupsToProcess.isEmpty()) {
String groupName = groupsToProcess.remove(0);
if (!checkedGroups.contains(groupName)) {
checkedGroups.add(groupName);
NuxeoGroup nxGroup = null;
if (userManager != null) {
try {
nxGroup = userManager.getGroup(groupName);
} catch (DirectoryException de) {
if (virtualGroups.contains(groupName)) {
// do not fail while retrieving a virtual group
log.warn("Failed to get group '" + groupName + "' due to '" + de.getMessage()
+ "': permission resolution involving groups may not be correct");
nxGroup = null;
} else {
throw de;
}
}
}
if (nxGroup == null) {
if (virtualGroups.contains(groupName)) {
// just add the virtual group as is
resultingGroups.add(groupName);
} else if (userManager != null) {
// XXX this should only happens in case of
// inconsistency in DB
log.error("User " + getName() + " references the " + groupName + " group that does not exists");
}
} else {
groupsToProcess.addAll(nxGroup.getParentGroups());
// fetch the group name from the returned entry in case
// it does not have the same case than the actual entry in
// directory (for case insensitive directories)
resultingGroups.add(nxGroup.getName());
// XXX: maybe remove group from virtual groups if it
// actually exists? otherwise it would be ignored when
// setting groups
}
}
}
allGroups = new ArrayList<String>(resultingGroups);
// set isAdministrator boolean according to groups declared on user
// manager
if (!isAdministrator() && userManager != null) {
List<String> adminGroups = userManager.getAdministratorsGroups();
for (String adminGroup : adminGroups) {
if (allGroups.contains(adminGroup)) {
isAdministrator = true;
break;
}
}
}
}
public List<String> getVirtualGroups() {
return new ArrayList<String>(virtualGroups);
}
public void setVirtualGroups(List<String> virtualGroups, boolean updateAllGroups) {
this.virtualGroups = new ArrayList<String>(virtualGroups);
if (updateAllGroups) {
updateAllGroups();
}
}
/**
* Sets virtual groups and recomputes all groups.
*/
public void setVirtualGroups(List<String> virtualGroups) {
setVirtualGroups(virtualGroups, true);
}
@Override
public boolean isAdministrator() {
return isAdministrator || SecurityConstants.SYSTEM_USERNAME.equals(getName());
}
@Override
public String getTenantId() {
return null;
}
@Override
public boolean isAnonymous() {
return isAnonymous;
}
@Override
public boolean equals(Object other) {
if (other instanceof Principal) {
String name = getName();
String otherName = ((Principal) other).getName();
if (name == null) {
return otherName == null;
} else {
return name.equals(otherName);
}
} else {
return false;
}
}
@Override
public int hashCode() {
String name = getName();
return name == null ? 0 : name.hashCode();
}
@Override
public String getOriginatingUser() {
return origUserName;
}
@Override
public void setOriginatingUser(String originatingUser) {
origUserName = originatingUser;
}
@Override
public String getActingUser() {
return getOriginatingUser() == null ? getName() : getOriginatingUser();
}
@Override
public boolean isTransient() {
String name = getName();
return name != null && name.startsWith(TRANSIENT_USER_PREFIX);
}
protected NuxeoPrincipal cloneTransferable() {
return new TransferableClone(this);
}
/**
* Provides another implementation which marshall the user id instead of
* transferring the whole content and resolve it when unmarshalled.
*
*/
static protected class TransferableClone extends NuxeoPrincipalImpl {
protected TransferableClone(NuxeoPrincipalImpl other) {
super(other);
}
static class DataTransferObject implements Serializable {
private static final long serialVersionUID = 1L;
final String username;
final String originatingUser;
DataTransferObject(NuxeoPrincipal principal) {
username = principal.getName();
originatingUser = principal.getOriginatingUser();
}
private Object readResolve() throws ObjectStreamException {
NuxeoPrincipal principal = Framework.getService(UserManager.class)
.getPrincipal(username);
principal.setOriginatingUser(originatingUser);
return principal;
}
}
private Object writeReplace() throws ObjectStreamException {
return new DataTransferObject(this);
}
}
}