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