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().values().iterator().next();
297        if (updateAllGroups) {
298            updateAllGroups();
299        }
300    }
301
302    @Override
303    public void setModel(DocumentModel model) {
304        setModel(model, true);
305    }
306
307    @Override
308    public boolean isMemberOf(String group) {
309        return allGroups.contains(group);
310    }
311
312    @Override
313    public List<String> getAllGroups() {
314        return new ArrayList<String>(allGroups);
315    }
316
317    public void updateAllGroups() {
318        UserManager userManager = Framework.getService(UserManager.class);
319        Set<String> checkedGroups = new HashSet<String>();
320        List<String> groupsToProcess = new ArrayList<String>();
321        List<String> resultingGroups = new ArrayList<String>();
322        groupsToProcess.addAll(getGroups());
323
324        while (!groupsToProcess.isEmpty()) {
325            String groupName = groupsToProcess.remove(0);
326            if (!checkedGroups.contains(groupName)) {
327                checkedGroups.add(groupName);
328                NuxeoGroup nxGroup = null;
329                if (userManager != null) {
330                    try {
331                        nxGroup = userManager.getGroup(groupName);
332                    } catch (DirectoryException de) {
333                        if (virtualGroups.contains(groupName)) {
334                            // do not fail while retrieving a virtual group
335                            log.warn("Failed to get group '" + groupName + "' due to '" + de.getMessage()
336                                    + "': permission resolution involving groups may not be correct");
337                            nxGroup = null;
338                        } else {
339                            throw de;
340                        }
341                    }
342                }
343                if (nxGroup == null) {
344                    if (virtualGroups.contains(groupName)) {
345                        // just add the virtual group as is
346                        resultingGroups.add(groupName);
347                    } else if (userManager != null) {
348                        // XXX this should only happens in case of
349                        // inconsistency in DB
350                        log.error("User " + getName() + " references the " + groupName + " group that does not exists");
351                    }
352                } else {
353                    groupsToProcess.addAll(nxGroup.getParentGroups());
354                    // fetch the group name from the returned entry in case
355                    // it does not have the same case than the actual entry in
356                    // directory (for case insensitive directories)
357                    resultingGroups.add(nxGroup.getName());
358                    // XXX: maybe remove group from virtual groups if it
359                    // actually exists? otherwise it would be ignored when
360                    // setting groups
361                }
362            }
363        }
364
365        allGroups = new ArrayList<String>(resultingGroups);
366
367        // set isAdministrator boolean according to groups declared on user
368        // manager
369        if (!isAdministrator() && userManager != null) {
370            List<String> adminGroups = userManager.getAdministratorsGroups();
371            for (String adminGroup : adminGroups) {
372                if (allGroups.contains(adminGroup)) {
373                    isAdministrator = true;
374                    break;
375                }
376            }
377        }
378    }
379
380    public List<String> getVirtualGroups() {
381        return new ArrayList<String>(virtualGroups);
382    }
383
384    public void setVirtualGroups(List<String> virtualGroups, boolean updateAllGroups) {
385        this.virtualGroups = new ArrayList<String>(virtualGroups);
386        if (updateAllGroups) {
387            updateAllGroups();
388        }
389    }
390
391    /**
392     * Sets virtual groups and recomputes all groups.
393     */
394    public void setVirtualGroups(List<String> virtualGroups) {
395        setVirtualGroups(virtualGroups, true);
396    }
397
398    @Override
399    public boolean isAdministrator() {
400        return isAdministrator || SecurityConstants.SYSTEM_USERNAME.equals(getName());
401    }
402
403    @Override
404    public String getTenantId() {
405        return null;
406    }
407
408    @Override
409    public boolean isAnonymous() {
410        return isAnonymous;
411    }
412
413    @Override
414    public boolean equals(Object other) {
415        if (other instanceof Principal) {
416            String name = getName();
417            String otherName = ((Principal) other).getName();
418            if (name == null) {
419                return otherName == null;
420            } else {
421                return name.equals(otherName);
422            }
423        } else {
424            return false;
425        }
426    }
427
428    @Override
429    public int hashCode() {
430        String name = getName();
431        return name == null ? 0 : name.hashCode();
432    }
433
434    @Override
435    public String getOriginatingUser() {
436        return origUserName;
437    }
438
439    @Override
440    public void setOriginatingUser(String originatingUser) {
441        origUserName = originatingUser;
442    }
443
444    @Override
445    public String getActingUser() {
446        return getOriginatingUser() == null ? getName() : getOriginatingUser();
447    }
448
449    @Override
450    public boolean isTransient() {
451        String name = getName();
452        return name != null && name.startsWith(TRANSIENT_USER_PREFIX);
453    }
454
455    static class DataTransferObject implements Serializable {
456
457        private static final long serialVersionUID = 1L;
458
459        final String username;
460
461        final String originatingUser;
462
463        DataTransferObject(NuxeoPrincipal principal) {
464            username = principal.getName();
465            originatingUser = principal.getOriginatingUser();
466        }
467
468        private Object readResolve() throws ObjectStreamException {
469            NuxeoPrincipal principal = Framework.getService(UserManager.class)
470                    .getPrincipal(username);
471            principal.setOriginatingUser(originatingUser);
472            return principal;
473        }
474
475    }
476
477    private Object writeReplace() throws ObjectStreamException {
478        if (true) {
479            return new DataTransferObject(this);
480        }
481        return this;
482    }
483}