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