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