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