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}