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.security.Principal;
034import java.util.ArrayList;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Set;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.core.api.DataModel;
044import org.nuxeo.ecm.core.api.DocumentModel;
045import org.nuxeo.ecm.core.api.NuxeoGroup;
046import org.nuxeo.ecm.core.api.NuxeoPrincipal;
047import org.nuxeo.ecm.core.api.PropertyException;
048import org.nuxeo.ecm.core.api.impl.DataModelImpl;
049import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
050import org.nuxeo.ecm.core.api.security.SecurityConstants;
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<String>();
063
064    // group not stored in the backend and added at login time
065    public List<String> virtualGroups = new LinkedList<String>();
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        DocumentModelImpl documentModelImpl = new DocumentModelImpl(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        documentModelImpl.addDataModel(new DataModelImpl(config.schemaName, new HashMap<String, Object>()));
109        setModel(documentModelImpl, updateAllGroups);
110        dataModel.setData(config.nameKey, name);
111        this.isAnonymous = isAnonymous;
112        this.isAdministrator = isAdministrator;
113    }
114
115    public void setConfig(UserConfig config) {
116        this.config = config;
117    }
118
119    public UserConfig getConfig() {
120        return config;
121    }
122
123    @Override
124    public String getCompany() {
125        try {
126            return (String) dataModel.getData(config.companyKey);
127        } catch (PropertyException e) {
128            return null;
129        }
130    }
131
132    @Override
133    public void setCompany(String company) {
134        dataModel.setData(config.companyKey, company);
135    }
136
137    @Override
138    public String getFirstName() {
139        try {
140            return (String) dataModel.getData(config.firstNameKey);
141        } catch (PropertyException e) {
142            return null;
143        }
144    }
145
146    @Override
147    public void setFirstName(String firstName) {
148        dataModel.setData(config.firstNameKey, firstName);
149    }
150
151    @Override
152    public String getLastName() {
153        try {
154            return (String) dataModel.getData(config.lastNameKey);
155        } catch (PropertyException e) {
156            return null;
157        }
158    }
159
160    @Override
161    public void setLastName(String lastName) {
162        dataModel.setData(config.lastNameKey, lastName);
163    }
164
165    // impossible to modify the name - it is PK
166    @Override
167    public void setName(String name) {
168        dataModel.setData(config.nameKey, name);
169    }
170
171    @Override
172    public void setRoles(List<String> roles) {
173        this.roles.clear();
174        this.roles.addAll(roles);
175    }
176
177    @Override
178    public void setGroups(List<String> groups) {
179        if (virtualGroups != null && !virtualGroups.isEmpty()) {
180            List<String> groupsToWrite = new ArrayList<String>();
181            for (String group : groups) {
182                if (!virtualGroups.contains(group)) {
183                    groupsToWrite.add(group);
184                }
185            }
186            dataModel.setData(config.groupsKey, groupsToWrite);
187        } else {
188            dataModel.setData(config.groupsKey, groups);
189        }
190    }
191
192    @Override
193    public String getName() {
194        try {
195            return (String) dataModel.getData(config.nameKey);
196        } catch (PropertyException e) {
197            return null;
198        }
199    }
200
201    @SuppressWarnings("unchecked")
202    @Override
203    public List<String> getGroups() {
204        List<String> groups = new LinkedList<String>();
205        List<String> storedGroups;
206        try {
207            storedGroups = (List<String>) dataModel.getData(config.groupsKey);
208        } catch (PropertyException e) {
209            return null;
210        }
211        if (storedGroups != null) {
212            groups.addAll(storedGroups);
213        }
214        groups.addAll(virtualGroups);
215        return groups;
216    }
217
218    @Deprecated
219    @Override
220    public List<String> getRoles() {
221        return new ArrayList<String>(roles);
222    }
223
224    @Override
225    public void setPassword(String password) {
226        dataModel.setData(config.passwordKey, password);
227    }
228
229    @Override
230    public String getPassword() {
231        // password should never be read at the UI level for safety reasons
232        // + backend directories usually only store hashes that are useless
233        // except to check authentication at the directory level
234        return null;
235    }
236
237    @Override
238    public String toString() {
239        return (String) dataModel.getData(config.nameKey);
240    }
241
242    @Override
243    public String getPrincipalId() {
244        return principalId;
245    }
246
247    @Override
248    public void setPrincipalId(String principalId) {
249        this.principalId = principalId;
250    }
251
252    @Override
253    public String getEmail() {
254        try {
255            return (String) dataModel.getData(config.emailKey);
256        } catch (PropertyException e) {
257            return null;
258        }
259    }
260
261    @Override
262    public void setEmail(String email) {
263        dataModel.setData(config.emailKey, email);
264    }
265
266    @Override
267    public DocumentModel getModel() {
268        return model;
269    }
270
271    /**
272     * Sets model and recomputes all groups.
273     */
274    public void setModel(DocumentModel model, boolean updateAllGroups) {
275        this.model = model;
276        dataModel = model.getDataModels().values().iterator().next();
277        if (updateAllGroups) {
278            updateAllGroups();
279        }
280    }
281
282    @Override
283    public void setModel(DocumentModel model) {
284        setModel(model, true);
285    }
286
287    @Override
288    public boolean isMemberOf(String group) {
289        return allGroups.contains(group);
290    }
291
292    @Override
293    public List<String> getAllGroups() {
294        return new ArrayList<String>(allGroups);
295    }
296
297    public void updateAllGroups() {
298        UserManager userManager = Framework.getService(UserManager.class);
299        Set<String> checkedGroups = new HashSet<String>();
300        List<String> groupsToProcess = new ArrayList<String>();
301        List<String> resultingGroups = new ArrayList<String>();
302        groupsToProcess.addAll(getGroups());
303
304        while (!groupsToProcess.isEmpty()) {
305            String groupName = groupsToProcess.remove(0);
306            if (!checkedGroups.contains(groupName)) {
307                checkedGroups.add(groupName);
308                NuxeoGroup nxGroup = null;
309                if (userManager != null) {
310                    try {
311                        nxGroup = userManager.getGroup(groupName);
312                    } catch (DirectoryException de) {
313                        if (virtualGroups.contains(groupName)) {
314                            // do not fail while retrieving a virtual group
315                            log.warn("Failed to get group '" + groupName + "' due to '" + de.getMessage()
316                                    + "': permission resolution involving groups may not be correct");
317                            nxGroup = null;
318                        } else {
319                            throw de;
320                        }
321                    }
322                }
323                if (nxGroup == null) {
324                    if (virtualGroups.contains(groupName)) {
325                        // just add the virtual group as is
326                        resultingGroups.add(groupName);
327                    } else if (userManager != null) {
328                        // XXX this should only happens in case of
329                        // inconsistency in DB
330                        log.error("User " + getName() + " references the " + groupName + " group that does not exists");
331                    }
332                } else {
333                    groupsToProcess.addAll(nxGroup.getParentGroups());
334                    // fetch the group name from the returned entry in case
335                    // it does not have the same case than the actual entry in
336                    // directory (for case insensitive directories)
337                    resultingGroups.add(nxGroup.getName());
338                    // XXX: maybe remove group from virtual groups if it
339                    // actually exists? otherwise it would be ignored when
340                    // setting groups
341                }
342            }
343        }
344
345        allGroups = new ArrayList<String>(resultingGroups);
346
347        // set isAdministrator boolean according to groups declared on user
348        // manager
349        if (!isAdministrator() && userManager != null) {
350            List<String> adminGroups = userManager.getAdministratorsGroups();
351            for (String adminGroup : adminGroups) {
352                if (allGroups.contains(adminGroup)) {
353                    isAdministrator = true;
354                    break;
355                }
356            }
357        }
358    }
359
360    public List<String> getVirtualGroups() {
361        return new ArrayList<String>(virtualGroups);
362    }
363
364    public void setVirtualGroups(List<String> virtualGroups, boolean updateAllGroups) {
365        this.virtualGroups = new ArrayList<String>(virtualGroups);
366        if (updateAllGroups) {
367            updateAllGroups();
368        }
369    }
370
371    /**
372     * Sets virtual groups and recomputes all groups.
373     */
374    public void setVirtualGroups(List<String> virtualGroups) {
375        setVirtualGroups(virtualGroups, true);
376    }
377
378    @Override
379    public boolean isAdministrator() {
380        return isAdministrator || SecurityConstants.SYSTEM_USERNAME.equals(getName());
381    }
382
383    @Override
384    public String getTenantId() {
385        return null;
386    }
387
388    @Override
389    public boolean isAnonymous() {
390        return isAnonymous;
391    }
392
393    @Override
394    public boolean equals(Object other) {
395        if (other instanceof Principal) {
396            String name = getName();
397            String otherName = ((Principal) other).getName();
398            if (name == null) {
399                return otherName == null;
400            } else {
401                return name.equals(otherName);
402            }
403        } else {
404            return false;
405        }
406    }
407
408    @Override
409    public int hashCode() {
410        String name = getName();
411        return name == null ? 0 : name.hashCode();
412    }
413
414    @Override
415    public String getOriginatingUser() {
416        return origUserName;
417    }
418
419    @Override
420    public void setOriginatingUser(String originatingUser) {
421        origUserName = originatingUser;
422    }
423
424    @Override
425    public String getActingUser() {
426        return getOriginatingUser() == null ? getName() : getOriginatingUser();
427    }
428
429    @Override
430    public boolean isTransient() {
431        String name = getName();
432        return name != null && name.startsWith(TRANSIENT_USER_PREFIX);
433    }
434}