001/*
002 * (C) Copyright 2015-2018 Nuxeo (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 *     dmetzler
018 *     Vladimir Pasquier <vpasquier@nuxeo.com>
019 *     Mincong Huang <mhuang@nuxeo.com>
020 *     Nuno Cunha <ncunha@nuxeo.com>
021 */
022
023package org.nuxeo.ecm.automation.core.operations.document;
024
025import static java.util.stream.Collectors.toList;
026import static org.nuxeo.ecm.core.api.NuxeoPrincipal.computeTransientUsername;
027import static org.nuxeo.ecm.core.api.NuxeoPrincipal.isTransientUsername;
028
029import java.io.Serializable;
030import java.util.ArrayList;
031import java.util.Calendar;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035
036import org.nuxeo.ecm.automation.core.Constants;
037import org.nuxeo.ecm.automation.core.annotations.Context;
038import org.nuxeo.ecm.automation.core.annotations.Operation;
039import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
040import org.nuxeo.ecm.automation.core.annotations.Param;
041import org.nuxeo.ecm.automation.core.collectors.DocumentModelCollector;
042import org.nuxeo.ecm.core.api.CoreSession;
043import org.nuxeo.ecm.core.api.DocumentModel;
044import org.nuxeo.ecm.core.api.DocumentRef;
045import org.nuxeo.ecm.core.api.security.ACE;
046import org.nuxeo.ecm.core.api.security.ACL;
047import org.nuxeo.ecm.core.api.security.ACP;
048import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
049import org.nuxeo.ecm.platform.usermanager.UserManager;
050import org.nuxeo.ecm.webengine.model.exceptions.IllegalParameterException;
051import org.nuxeo.runtime.api.Framework;
052import org.nuxeo.runtime.services.config.ConfigurationService;
053
054/**
055 * Operation that adds a permission to a given ACL for a given user.
056 *
057 * @since 5.7.3
058 */
059@Operation(id = AddPermission.ID, category = Constants.CAT_DOCUMENT, label = "Add Permission", description = "Add Permission on the input document(s). Returns the document(s).", aliases = {
060        "Document.AddACL" })
061public class AddPermission {
062
063    public static final String ID = "Document.AddPermission";
064
065    public static final String NOTIFY_KEY = "notify";
066
067    public static final String COMMENT_KEY = "comment";
068
069    /**
070     * Configuration property name, which defines whether virtual user (non-existent user) is allowed in Nuxeo
071     * automation. If allowed, Nuxeo server will not check the user existence during automation execution. Set this
072     * property to true if you use Nuxeo computed user or computed group.
073     *
074     * @since 9.1
075     */
076    public static final String ALLOW_VIRTUAL_USER = "nuxeo.automation.allowVirtualUser";
077
078    @Context
079    protected CoreSession session;
080
081    /**
082     * @since 10.3
083     */
084    @Param(name = "users", required = false, alias = "users", description = "ACE target set of users and/or groups.")
085    protected List<String> users;
086
087    /**
088     * @deprecated since 10.3, use {@link #users} instead.
089     */
090    @Deprecated
091    @Param(name = "username", required = false, alias = "user", description = "ACE target user/group.")
092    protected String user;
093
094    /**
095     * @since 8.1
096     */
097    @Param(name = "email", required = false, description = "ACE target user/group.")
098    protected String email;
099
100    @Param(name = "permission", description = "ACE permission.")
101    protected String permission;
102
103    @Param(name = "acl", required = false, values = { ACL.LOCAL_ACL }, description = "ACL name.")
104    protected String aclName = ACL.LOCAL_ACL;
105
106    @Param(name = "begin", required = false, description = "ACE begin date.")
107    protected Calendar begin;
108
109    @Param(name = "end", required = false, description = "ACE end date.")
110    protected Calendar end;
111
112    @Param(name = "blockInheritance", required = false, description = "Block inheritance or not.")
113    protected boolean blockInheritance = false;
114
115    @Param(name = "notify", required = false, description = "Notify the user or not")
116    protected boolean notify = false;
117
118    @Param(name = "comment", required = false, description = "Comment")
119    protected String comment;
120
121    @OperationMethod(collector = DocumentModelCollector.class)
122    public DocumentModel run(DocumentModel doc) {
123        validateParameters();
124        addPermission(doc);
125        return doc;
126    }
127
128    @OperationMethod(collector = DocumentModelCollector.class)
129    public DocumentModel run(DocumentRef docRef) {
130        DocumentModel doc = session.getDocument(docRef);
131        validateParameters();
132        addPermission(doc);
133        return doc;
134    }
135
136    protected void addPermission(DocumentModel doc) {
137        ACP acp = doc.getACP() != null ? doc.getACP() : new ACPImpl();
138        Map<String, Serializable> contextData = new HashMap<>();
139        contextData.put(NOTIFY_KEY, notify);
140        contextData.put(COMMENT_KEY, comment);
141
142        String creator = session.getPrincipal().getName();
143        boolean permissionChanged = false;
144        if (blockInheritance) {
145            permissionChanged = acp.blockInheritance(aclName, creator);
146        }
147
148        for (String username : users) {
149            ACE ace = ACE.builder(username, permission)
150                         .creator(creator)
151                         .begin(begin)
152                         .end(end)
153                         .contextData(contextData)
154                         .build();
155            permissionChanged = acp.addACE(aclName, ace) || permissionChanged;
156        }
157
158        if (permissionChanged) {
159            doc.setACP(acp, true);
160        }
161    }
162
163    protected void validateParameters() {
164        if (user == null && (users == null || users.isEmpty()) && email == null) {
165            throw new IllegalParameterException("'users' or 'email' parameters must be set");
166        } else if (email != null && end == null) {
167            throw new IllegalParameterException("'end' parameter must be set when adding a permission for an 'email'");
168        }
169
170        ensureUserListIsUsed();
171
172        ConfigurationService configService = Framework.getService(ConfigurationService.class);
173        if (configService.isBooleanPropertyFalse(ALLOW_VIRTUAL_USER)) {
174            UserManager userManager = Framework.getService(UserManager.class);
175
176            List<String> unknownNames = users.stream()
177                                             .filter(userOrGroupName -> !isTransientUsername(userOrGroupName)
178                                                     && userManager.getUserModel(userOrGroupName) == null
179                                                     && userManager.getGroupModel(userOrGroupName) == null)
180                                             .collect(toList());
181
182            if (!unknownNames.isEmpty()) {
183                String errorMsg = String.format(
184                        "The following set of User or Group names do not exist: [%s]. Please provide valid ones.",
185                        String.join(",", unknownNames));
186                throw new IllegalParameterException(errorMsg);
187            }
188        }
189    }
190
191    /**
192     * Method to help deprecating {@link #user} parameter.
193     */
194    protected void ensureUserListIsUsed() {
195        users = users == null ? new ArrayList<>() : new ArrayList<>(users);
196
197        if (user != null && !users.contains(user)) {
198            users.add(user);
199        } else if (email != null && users.isEmpty()) {
200            // share a document with someone not registered in Nuxeo, by using only an email
201            users.add(computeTransientUsername(email));
202        }
203    }
204
205}