001/*
002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.webapp.security;
021
022import static org.jboss.seam.ScopeType.CONVERSATION;
023
024import java.io.Serializable;
025import java.security.Principal;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031
032import javax.faces.context.FacesContext;
033import javax.faces.model.SelectItem;
034import javax.servlet.http.HttpServletRequest;
035
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.jboss.seam.ScopeType;
040import org.jboss.seam.annotations.Factory;
041import org.jboss.seam.annotations.In;
042import org.jboss.seam.annotations.Name;
043import org.jboss.seam.annotations.Observer;
044import org.jboss.seam.annotations.Scope;
045import org.jboss.seam.annotations.intercept.BypassInterceptors;
046import org.jboss.seam.core.Events;
047import org.jboss.seam.faces.FacesMessages;
048import org.jboss.seam.international.StatusMessage;
049import org.nuxeo.common.utils.UserAgentMatcher;
050import org.nuxeo.common.utils.i18n.Labeler;
051import org.nuxeo.ecm.core.api.CoreSession;
052import org.nuxeo.ecm.core.api.DocumentModel;
053import org.nuxeo.ecm.core.api.NuxeoPrincipal;
054import org.nuxeo.ecm.core.api.security.ACP;
055import org.nuxeo.ecm.core.api.security.PermissionProvider;
056import org.nuxeo.ecm.core.api.security.SecurityConstants;
057import org.nuxeo.ecm.core.api.security.UserEntry;
058import org.nuxeo.ecm.core.api.security.UserVisiblePermission;
059import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
060import org.nuxeo.ecm.platform.query.api.PageSelection;
061import org.nuxeo.ecm.platform.query.api.PageSelections;
062import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
063import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
064import org.nuxeo.ecm.platform.usermanager.UserManager;
065import org.nuxeo.ecm.webapp.base.InputController;
066import org.nuxeo.ecm.webapp.helpers.EventNames;
067import org.nuxeo.runtime.api.Framework;
068
069/**
070 * Provides security related methods.
071 *
072 * @author Razvan Caraghin
073 */
074@Name("securityActions")
075@Scope(CONVERSATION)
076public class SecurityActionsBean extends InputController implements SecurityActions, Serializable {
077
078    private static final long serialVersionUID = -7190826911734958662L;
079
080    private static final Log log = LogFactory.getLog(SecurityActionsBean.class);
081
082    @In(create = true)
083    protected transient NavigationContext navigationContext;
084
085    @In(create = true, required = false)
086    protected transient CoreSession documentManager;
087
088    @In(create = true)
089    protected PermissionActionListManager permissionActionListManager;
090
091    @In(create = true)
092    protected PermissionListManager permissionListManager;
093
094    @In(create = true)
095    protected PrincipalListManager principalListManager;
096
097    @In(create = true)
098    protected transient UserManager userManager;
099
100    @In(create = true)
101    protected NuxeoPrincipal currentUser;
102
103    protected static final String[] SEED_PERMISSIONS_TO_CHECK = { SecurityConstants.WRITE_SECURITY,
104            SecurityConstants.READ_SECURITY };
105
106    private static final Labeler labeler = new Labeler("label.security.permission");
107
108    protected String[] CACHED_PERMISSION_TO_CHECK;
109
110    protected SecurityData securityData;
111
112    protected boolean obsoleteSecurityData = true;
113
114    protected PageSelections<String> entries;
115
116    protected transient List<String> cachedValidatedUserAndGroups;
117
118    protected transient List<String> cachedDeletedUserAndGroups;
119
120    private Boolean blockRightInheritance;
121
122    protected String selectedEntry;
123
124    protected List<String> selectedEntries;
125
126    @Override
127    @Observer(value = EventNames.USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED, create = false)
128    @BypassInterceptors
129    public void resetSecurityData() {
130        obsoleteSecurityData = true;
131        blockRightInheritance = null;
132    }
133
134    @Override
135    public void rebuildSecurityData() {
136        DocumentModel currentDocument = navigationContext.getCurrentDocument();
137        if (currentDocument != null) {
138            if (securityData == null) {
139                securityData = new SecurityData();
140                securityData.setDocumentType(currentDocument.getType());
141            }
142            ACP acp = documentManager.getACP(currentDocument.getRef());
143
144            if (acp != null) {
145                SecurityDataConverter.convertToSecurityData(acp, securityData);
146            } else {
147                securityData.clear();
148            }
149
150            reconstructTableModel();
151
152            // Check if the inherited rights are activated
153            List<String> deniedPerms = securityData.getCurrentDocDeny().get(SecurityConstants.EVERYONE);
154            if (deniedPerms != null && deniedPerms.contains(SecurityConstants.EVERYTHING)) {
155                blockRightInheritance = Boolean.TRUE;
156            }
157
158            if (blockRightInheritance == null) {
159                blockRightInheritance = Boolean.FALSE;
160            }
161            obsoleteSecurityData = false;
162        }
163    }
164
165    /**
166     * Update the dataTableModel from the current {@link SecurityData} this method is automatically called by
167     * rebuildSecurityData method
168     */
169    protected void reconstructTableModel() {
170        List<String> items = getCurrentDocumentUsers();
171        entries = new PageSelections<String>();
172        if (items != null) {
173            for (String item : items) {
174                if (SecurityConstants.EVERYONE.equals(item)) {
175                    final List<String> grantedPerms = securityData.getCurrentDocGrant().get(item);
176                    final List<String> deniedPerms = securityData.getCurrentDocDeny().get(item);
177                    if (deniedPerms != null && deniedPerms.contains(SecurityConstants.EVERYTHING)
178                            && grantedPerms == null && deniedPerms.size() == 1) {
179                        // the only perm is deny everything, there is no need to display the row
180                        continue;
181                    }
182                }
183                entries.add(new PageSelection<String>(item, false));
184            }
185        }
186    }
187
188    @Override
189    public PageSelections<String> getDataTableModel() {
190        if (obsoleteSecurityData) {
191            // lazy initialization at first time access
192            rebuildSecurityData();
193        }
194
195        return entries;
196    }
197
198    @Override
199    public SecurityData getSecurityData() {
200        if (obsoleteSecurityData) {
201            // lazy initialization at first time access
202            rebuildSecurityData();
203        }
204        return securityData;
205    }
206
207    @Override
208    public String updateSecurityOnDocument() {
209        List<UserEntry> modifiableEntries = SecurityDataConverter.convertToUserEntries(securityData);
210        ACP acp = currentDocument.getACP();
211
212        if (acp == null) {
213            acp = new ACPImpl();
214        }
215
216        acp.setRules(modifiableEntries.toArray(new UserEntry[0]));
217
218        currentDocument.setACP(acp, true);
219        documentManager.save();
220        Events.instance().raiseEvent(EventNames.DOCUMENT_SECURITY_CHANGED);
221
222        // Reread data from the backend to be sure the current bean
223        // state is uptodate w.r.t. the real backend state
224        rebuildSecurityData();
225
226        // Type currentType = typeManager.getType(currentDocument.getType());
227        // return applicationController.getPageOnEditedDocumentType(currentType);
228
229        // Forward to default view, that's not what we want
230        // return navigationContext.getActionResult(currentDocument, UserAction.AFTER_EDIT);
231
232        // Temporary fix, to avoid forward to default_view.
233        // The same page is reloaded after submit.
234        // May use UserAction, with new kind of action (AFTER_EDIT_RIGHTS)?
235        return null;
236    }
237
238    @Override
239    public String addPermission(String principalName, String permissionName, boolean grant) {
240        if (securityData == null) {
241            securityData = getSecurityData();
242        }
243
244        String grantPerm = permissionName;
245        String denyPerm = permissionName;
246        List<UserVisiblePermission> uvps = getVisibleUserPermissions(securityData.getDocumentType());
247        if (uvps != null) {
248            for (UserVisiblePermission uvp : uvps) {
249                if (uvp.getId().equals(permissionName)) {
250                    grantPerm = uvp.getPermission();
251                    denyPerm = uvp.getDenyPermission();
252                    break;
253                }
254            }
255        } else {
256            log.debug("no entry for documentType in visibleUserPermissions this should never happend, using default mapping ...");
257        }
258
259        if (grant) {
260            // remove the opposite rule if any
261            boolean removed = securityData.removeModifiablePrivilege(principalName, denyPerm, !grant);
262            if (!removed) {
263                removed = securityData.removeModifiablePrivilege(principalName, grantPerm, !grant);
264            }
265            // add rule only if none was removed
266            if (!removed) {
267                securityData.addModifiablePrivilege(principalName, grantPerm, grant);
268            }
269        } else {
270            // remove the opposite rule if any
271            boolean removed = securityData.removeModifiablePrivilege(principalName, grantPerm, !grant);
272            if (!removed) {
273                removed = securityData.removeModifiablePrivilege(principalName, denyPerm, !grant);
274            }
275            // add rule only if none was removed
276            if (!removed) {
277                securityData.addModifiablePrivilege(principalName, denyPerm, grant);
278            }
279        }
280        reconstructTableModel();
281        return null;
282    }
283
284    @Override
285    public String addPermission() {
286        String permissionName = permissionListManager.getSelectedPermission();
287        boolean grant = permissionActionListManager.getSelectedGrant().equals("Grant");
288        return addPermission(selectedEntry, permissionName, grant);
289    }
290
291    @Override
292    public String addPermissions() {
293        if (selectedEntries == null || selectedEntries.isEmpty()) {
294            String message = ComponentUtils.translate(FacesContext.getCurrentInstance(),
295                    "error.rightsManager.noUsersSelected");
296            FacesMessages.instance().add(message);
297            return null;
298        }
299        String permissionName = permissionListManager.getSelectedPermission();
300        boolean grant = permissionActionListManager.getSelectedGrant().equals("Grant");
301
302        for (String principalName : selectedEntries) {
303            addPermission(principalName, permissionName, grant);
304        }
305        return null;
306    }
307
308    @Override
309    public String addPermissionAndUpdate() {
310        addPermission();
311        updateSecurityOnDocument();
312        return null;
313    }
314
315    @Override
316    public String addPermissionsAndUpdate() {
317        addPermissions();
318        updateSecurityOnDocument();
319        selectedEntries = null;
320        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get("message.updated.rights"));
321        return null;
322    }
323
324    @Override
325    public String saveSecurityUpdates() {
326        updateSecurityOnDocument();
327        selectedEntries = null;
328        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get("message.updated.rights"));
329        return null;
330    }
331
332    @Override
333    public String removePermission() {
334        securityData.removeModifiablePrivilege(selectedEntry, permissionListManager.getSelectedPermission(),
335                permissionActionListManager.getSelectedGrant().equals("Grant"));
336        reconstructTableModel();
337        return null;
338    }
339
340    @Override
341    public String removePermissionAndUpdate() {
342        removePermission();
343
344        if (!checkPermissions()) {
345            facesMessages.add(StatusMessage.Severity.ERROR,
346                    resourcesAccessor.getMessages().get("message.updated.rights"));
347            return null;
348        }
349
350        updateSecurityOnDocument();
351        // do not redirect to the default folder view
352        return null;
353    }
354
355    @Override
356    public String removePermissions() {
357        for (PageSelection<String> user : getSelectedRows()) {
358            securityData.removeModifiablePrivilege(user.getData());
359            if (!checkPermissions()) {
360                facesMessages.add(StatusMessage.Severity.ERROR,
361                        resourcesAccessor.getMessages().get("message.error.removeRight"));
362                return null;
363            }
364        }
365        reconstructTableModel();
366        return null;
367    }
368
369    @Override
370    public String removePermissionsAndUpdate() {
371        for (PageSelection<String> user : getDataTableModel().getEntries()) {
372            securityData.removeModifiablePrivilege(user.getData());
373            if (!checkPermissions()) {
374                facesMessages.add(StatusMessage.Severity.ERROR,
375                        resourcesAccessor.getMessages().get("message.error.removeRight"));
376                return null;
377            }
378        }
379        updateSecurityOnDocument();
380        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get("message.updated.rights"));
381        // do not redirect to the default folder view
382        return null;
383    }
384
385    @Override
386    public boolean getCanAddSecurityRules() {
387        return documentManager.hasPermission(currentDocument.getRef(), "WriteSecurity");
388    }
389
390    @Override
391    public boolean getCanRemoveSecurityRules() {
392        return documentManager.hasPermission(currentDocument.getRef(), "WriteSecurity") && !getSelectedRows().isEmpty();
393    }
394
395    /**
396     * @return The list of selected rows in the local rights table.
397     * @since 6.0
398     */
399    private List<PageSelection<String>> getSelectedRows() {
400        List<PageSelection<String>> selectedRows = new ArrayList<PageSelection<String>>();
401
402        if (!getDataTableModel().isEmpty()) {
403            for (PageSelection<String> entry : getDataTableModel().getEntries()) {
404                if (entry.isSelected()) {
405                    selectedRows.add(entry);
406                }
407            }
408        }
409        return selectedRows;
410    }
411
412    public List<UserVisiblePermission> getVisibleUserPermissions(String documentType) {
413        return Framework.getService(PermissionProvider.class).getUserVisiblePermissionDescriptors(documentType);
414    }
415
416    @Override
417    public List<SelectItem> getSettablePermissions() {
418        String documentType = navigationContext.getCurrentDocument().getType();
419
420        // BBB: use the platform service if it defines permissions (deprecated)
421        UIPermissionService service = (UIPermissionService) Framework.getRuntime().getComponent(
422                UIPermissionService.NAME);
423        String[] settablePermissions = service.getUIPermissions(documentType);
424
425        if (settablePermissions == null || settablePermissions.length == 0) {
426            // new centralized permission provider at the core level
427
428            List<UserVisiblePermission> visiblePerms = getVisibleUserPermissions(documentType);
429            settablePermissions = new String[visiblePerms.size()];
430            int idx = 0;
431            for (UserVisiblePermission uvp : visiblePerms) {
432                settablePermissions[idx] = uvp.getId();
433                idx++;
434            }
435        }
436
437        return asSelectItems(settablePermissions);
438    }
439
440    protected List<SelectItem> asSelectItems(String... permissions) {
441        List<SelectItem> items = new ArrayList<SelectItem>();
442        for (String perm : permissions) {
443            String label = labeler.makeLabel(perm);
444            SelectItem it = new SelectItem(perm, resourcesAccessor.getMessages().get(label));
445            items.add(it);
446        }
447        return items;
448    }
449
450    /**
451     * @since 7.4
452     */
453    public List<SelectItem> getUserVisiblePermissionSelectItems(String documentType) {
454        List<UserVisiblePermission> userVisiblePermissions = getVisibleUserPermissions(documentType);
455        List<String> permissions = new ArrayList<>();
456        for (UserVisiblePermission userVisiblePermission : userVisiblePermissions) {
457            permissions.add(userVisiblePermission.getId());
458        }
459        return asSelectItems(permissions.toArray(new String[permissions.size()]));
460    }
461
462    @Override
463    public Map<String, String> getIconAltMap() {
464        return principalListManager.iconAlt;
465    }
466
467    @Override
468    public Map<String, String> getIconPathMap() {
469        return principalListManager.iconPath;
470    }
471
472    @Override
473    public Boolean getBlockRightInheritance() {
474        return blockRightInheritance;
475    }
476
477    @Override
478    public void setBlockRightInheritance(Boolean blockRightInheritance) {
479        this.blockRightInheritance = blockRightInheritance;
480    }
481
482    public String blockRightInheritance() {
483        Boolean needBlockRightInheritance = blockRightInheritance;
484
485        if (needBlockRightInheritance) {
486            // Block
487            securityData.addModifiablePrivilege(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false);
488            // add user to avoid lock up
489            Principal currentUser = FacesContext.getCurrentInstance().getExternalContext().getUserPrincipal();
490            if (securityData.getCurrentDocumentUsers() != null
491                    && !securityData.getCurrentDocumentUsers().contains(currentUser.getName())) {
492                securityData.addModifiablePrivilege(currentUser.getName(), SecurityConstants.EVERYTHING, true);
493                // add administrators to avoid LockUp
494                List<String> adminGroups = userManager.getAdministratorsGroups();
495                for (String adminGroup : adminGroups) {
496                    securityData.addModifiablePrivilege(adminGroup, SecurityConstants.EVERYTHING, true);
497                }
498            }
499        } else {
500            securityData.removeModifiablePrivilege(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false);
501        }
502        updateSecurityOnDocument();
503        selectedEntries = null;
504        return null;
505    }
506
507    @Override
508    public Boolean displayInheritedPermissions() {
509        return getDisplayInheritedPermissions();
510    }
511
512    @Override
513    public boolean getDisplayInheritedPermissions() {
514        if (blockRightInheritance == null) {
515            rebuildSecurityData();
516        }
517        if (blockRightInheritance) {
518            return false;
519        }
520        return !securityData.getParentDocumentsUsers().isEmpty();
521    }
522
523    @Override
524    public List<String> getCurrentDocumentUsers() {
525        List<String> currentUsers = securityData.getCurrentDocumentUsers();
526        return validateUserGroupList(currentUsers);
527    }
528
529    @Override
530    public List<String> getParentDocumentsUsers() {
531        List<String> parentUsers = securityData.getParentDocumentsUsers();
532        return validateUserGroupList(parentUsers);
533    }
534
535    /**
536     * Validates user/group against userManager in order to remove obsolete entries (ie: deleted groups/users).
537     */
538    private List<String> validateUserGroupList(List<String> usersGroups2Validate) {
539        // TODO :
540        // 1 -should add a clean cache system to avoid
541        // calling the directory : this can be problematic for big ldaps
542        // 2 - this filtering should at some point be applied to acp and saved
543        // back in a batch?
544
545        List<String> returnList = new ArrayList<String>();
546        for (String entry : usersGroups2Validate) {
547            if (entry.equals(SecurityConstants.EVERYONE)) {
548                returnList.add(entry);
549                continue;
550            }
551            if (isUserGroupInCache(entry)) {
552                returnList.add(entry);
553                continue;
554            }
555            if (isUserGroupInDeletedCache(entry)) {
556                continue;
557            }
558
559            if (userManager.getPrincipal(entry) != null) {
560                returnList.add(entry);
561                addUserGroupInCache(entry);
562                continue;
563            } else if (userManager.getGroup(entry) != null) {
564                returnList.add(entry);
565                addUserGroupInCache(entry);
566                continue;
567            } else {
568                addUserGroupInDeletedCache(entry);
569            }
570        }
571        return returnList;
572    }
573
574    private Boolean isUserGroupInCache(String entry) {
575        if (cachedValidatedUserAndGroups == null) {
576            return false;
577        }
578        return cachedValidatedUserAndGroups.contains(entry);
579    }
580
581    private void addUserGroupInCache(String entry) {
582        if (cachedValidatedUserAndGroups == null) {
583            cachedValidatedUserAndGroups = new ArrayList<String>();
584        }
585        cachedValidatedUserAndGroups.add(entry);
586    }
587
588    private Boolean isUserGroupInDeletedCache(String entry) {
589        if (cachedDeletedUserAndGroups == null) {
590            return false;
591        }
592        return cachedDeletedUserAndGroups.contains(entry);
593    }
594
595    private void addUserGroupInDeletedCache(String entry) {
596        if (cachedDeletedUserAndGroups == null) {
597            cachedDeletedUserAndGroups = new ArrayList<String>();
598        }
599
600        cachedDeletedUserAndGroups.add(entry);
601    }
602
603    /**
604     * Checks if the current user can still read and write access rights. If he can't, then the security data are
605     * rebuilt.
606     */
607    private boolean checkPermissions() {
608        if (currentUser.isAdministrator()) {
609            return true;
610        } else {
611            List<String> principals = new ArrayList<String>();
612            principals.add(currentUser.getName());
613            principals.addAll(currentUser.getAllGroups());
614
615            ACP acp = currentDocument.getACP();
616            new SecurityDataConverter();
617            List<UserEntry> modifiableEntries = SecurityDataConverter.convertToUserEntries(securityData);
618
619            if (null == acp) {
620                acp = new ACPImpl();
621            }
622            acp.setRules(modifiableEntries.toArray(new UserEntry[0]));
623
624            final boolean access = acp.getAccess(principals.toArray(new String[0]), getPermissionsToCheck())
625                                      .toBoolean();
626            if (!access) {
627                rebuildSecurityData();
628            }
629            return access;
630        }
631    }
632
633    protected String[] getPermissionsToCheck() {
634        if (CACHED_PERMISSION_TO_CHECK == null) {
635            PermissionProvider pprovider = Framework.getService(PermissionProvider.class);
636            List<String> aggregatedPerms = new LinkedList<String>();
637            for (String seedPerm : SEED_PERMISSIONS_TO_CHECK) {
638                aggregatedPerms.add(seedPerm);
639                String[] compoundPerms = pprovider.getPermissionGroups(seedPerm);
640                if (compoundPerms != null) {
641                    aggregatedPerms.addAll(Arrays.asList(compoundPerms));
642                }
643            }
644            CACHED_PERMISSION_TO_CHECK = aggregatedPerms.toArray(new String[aggregatedPerms.size()]);
645        }
646        return CACHED_PERMISSION_TO_CHECK;
647    }
648
649    @Override
650    public String getSelectedEntry() {
651        return selectedEntry;
652    }
653
654    @Override
655    public void setSelectedEntry(String selectedEntry) {
656        this.selectedEntry = selectedEntry;
657    }
658
659    @Override
660    public List<String> getSelectedEntries() {
661        return selectedEntries;
662    }
663
664    @Override
665    public void setSelectedEntries(List<String> selectedEntries) {
666        this.selectedEntries = selectedEntries;
667    }
668
669    @Factory(value = "isMSIEorEdge", scope = ScopeType.SESSION)
670    public boolean isMSIEorEdge() {
671        FacesContext context = FacesContext.getCurrentInstance();
672        if (context != null) {
673            HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
674            String ua = request.getHeader("User-Agent");
675            return UserAgentMatcher.isMSIE6or7(ua) || UserAgentMatcher.isMSIE10OrMore(ua)
676                    || UserAgentMatcher.isMSEdge(ua);
677        } else {
678            return false;
679        }
680    }
681
682    public String getLabel(String permission) {
683        return StringUtils.isNotBlank(permission) ? labeler.makeLabel(permission) : permission;
684    }
685
686}