001/*
002 * (C) Copyright 2016-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 *     Michael Vachette
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.automation.core.operations.users;
021
022import java.util.AbstractMap.SimpleEntry;
023import java.util.Arrays;
024import java.util.HashSet;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map.Entry;
028import java.util.Queue;
029import java.util.Set;
030
031import javax.servlet.http.HttpServletResponse;
032
033import org.apache.commons.lang3.StringUtils;
034import org.nuxeo.ecm.automation.OperationContext;
035import org.nuxeo.ecm.automation.OperationException;
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.util.Properties;
042import org.nuxeo.ecm.automation.core.util.StringList;
043import org.nuxeo.ecm.core.api.DocumentModel;
044import org.nuxeo.ecm.core.api.NuxeoException;
045import org.nuxeo.ecm.core.api.NuxeoGroup;
046import org.nuxeo.ecm.core.api.NuxeoPrincipal;
047import org.nuxeo.ecm.directory.BaseSession;
048import org.nuxeo.ecm.platform.usermanager.GroupConfig;
049import org.nuxeo.ecm.platform.usermanager.NuxeoGroupImpl;
050import org.nuxeo.ecm.platform.usermanager.UserManager;
051
052/**
053 * Operation to create or update a group.
054 *
055 * @since 9.1
056 */
057@Operation(id = CreateOrUpdateGroup.ID, //
058        category = Constants.CAT_USERS_GROUPS, //
059        label = "Create or Update Group", //
060        description = "Create or Update Group")
061public class CreateOrUpdateGroup {
062
063    public static final String ID = "Group.CreateOrUpdate";
064
065    public static final String CREATE_OR_UPDATE = "createOrUpdate";
066
067    public static final String CREATE = "create";
068
069    public static final String UPDATE = "update";
070
071    public static final String GROUP_SCHEMA = "group";
072
073    protected static final String GROUP_COLON = GROUP_SCHEMA + ':';
074
075    public static final String GROUP_NAME = "groupname";
076
077    public static final String GROUP_LABEL = "grouplabel";
078
079    public static final String GROUP_DESCRIPTION = "description";
080
081    public static final String MEMBERS = "members";
082
083    public static final String SUB_GROUPS = "subGroups";
084
085    public static final String PARENT_GROUPS = "parentGroups";
086
087    public static final String GROUP_TENANTID = "tenantId";
088
089    @Context
090    protected UserManager userManager;
091
092    @Context
093    protected OperationContext ctx;
094
095    @Param(name = "groupname")
096    protected String groupName;
097
098    @Param(name = "tenantId", required = false)
099    protected String tenantId;
100
101    @Param(name = "grouplabel", required = false)
102    protected String groupLabel;
103
104    @Param(name = "description", required = false)
105    protected String groupDescription;
106
107    @Param(name = "members", required = false)
108    protected StringList members;
109
110    @Param(name = "subGroups", required = false)
111    protected StringList subGroups;
112
113    @Param(name = "parentGroups", required = false)
114    protected StringList parentGroups;
115
116    @Param(name = "properties", required = false)
117    protected Properties properties = new Properties();
118
119    @Param(name = "mode", required = false, values = { CREATE_OR_UPDATE, CREATE, UPDATE })
120    protected String mode;
121
122    @OperationMethod
123    public void run() throws OperationException {
124        String tenantGroupName = getTenantGroupName(groupName, tenantId);
125        boolean create;
126        DocumentModel groupDoc = userManager.getGroupModel(tenantGroupName);
127        if (groupDoc == null) {
128            if (UPDATE.equals(mode)) {
129                throw new OperationException("Cannot update non-existent group: " + groupName);
130            }
131            create = true;
132            groupDoc = userManager.getBareGroupModel();
133            groupDoc.setProperty(GROUP_SCHEMA, GROUP_NAME, tenantGroupName);
134        } else {
135            if (CREATE.equals(mode)) {
136                throw new OperationException("Cannot create already-existing group: " + groupName);
137            }
138            create = false;
139
140            // make sure that the group can be updated
141            checkCanCreateOrUpdateGroup(groupDoc);
142        }
143        if (members != null) {
144            groupDoc.setProperty(GROUP_SCHEMA, MEMBERS, members);
145        }
146        if (subGroups != null) {
147            groupDoc.setProperty(GROUP_SCHEMA, SUB_GROUPS, subGroups);
148        }
149        if (parentGroups != null) {
150            groupDoc.setProperty(GROUP_SCHEMA, PARENT_GROUPS, parentGroups);
151        }
152        for (Entry<String, String> entry : Arrays.asList( //
153                new SimpleEntry<>(GROUP_TENANTID, tenantId), //
154                new SimpleEntry<>(GROUP_LABEL, groupLabel), //
155                new SimpleEntry<>(GROUP_DESCRIPTION, groupDescription))) {
156            String key = entry.getKey();
157            String value = entry.getValue();
158            if (StringUtils.isNotBlank(value)) {
159                properties.put(key, value);
160            }
161        }
162        for (Entry<String, String> entry : properties.entrySet()) {
163            String key = entry.getKey();
164            String value = entry.getValue();
165            if (key.startsWith(GROUP_COLON)) {
166                key = key.substring(GROUP_COLON.length());
167            }
168            groupDoc.setProperty(GROUP_SCHEMA, key, value);
169        }
170
171        // make sure that the new group can be created or updated
172        checkCanCreateOrUpdateGroup(groupDoc);
173
174        if (create) {
175            userManager.createGroup(groupDoc);
176        } else {
177            userManager.updateGroup(groupDoc);
178        }
179    }
180
181    /**
182     * Use tenant_mytenant_mygroup instead of mygroup for groups having a tenant id.
183     * <p>
184     * This is done explicitly instead of the implicit computation done in SQLSession.createEntry which is based on a
185     * tenant id deduced from the logged-in user.
186     */
187    public static String getTenantGroupName(String groupName, String tenantId) {
188        if (StringUtils.isBlank(tenantId)) {
189            return groupName;
190        }
191        return BaseSession.computeMultiTenantDirectoryId(tenantId, groupName);
192    }
193
194    protected void checkCanCreateOrUpdateGroup(DocumentModel groupDoc) {
195        NuxeoPrincipal currentUser = ctx.getPrincipal();
196        if (!currentUser.isAdministrator()
197                && (!currentUser.isMemberOf("powerusers") || !canCreateOrUpdateGroup(groupDoc))) {
198            throw new NuxeoException("User is not allowed to create or edit groups", HttpServletResponse.SC_FORBIDDEN);
199        }
200    }
201
202    protected boolean canCreateOrUpdateGroup(DocumentModel groupDoc) {
203        GroupConfig groupConfig = userManager.getGroupConfig();
204        NuxeoGroup group = new NuxeoGroupImpl(groupDoc, groupConfig);
205        Set<String> allGroups = computeAllGroups(group);
206        List<String> administratorsGroups = userManager.getAdministratorsGroups();
207        return allGroups.stream().noneMatch(administratorsGroups::contains);
208    }
209
210    protected Set<String> computeAllGroups(NuxeoGroup group) {
211        Set<String> allGroups = new HashSet<>();
212        Queue<NuxeoGroup> queue = new LinkedList<>();
213        queue.add(group);
214
215        while (!queue.isEmpty()) {
216            NuxeoGroup nuxeoGroup = queue.poll();
217            allGroups.add(nuxeoGroup.getName());
218            nuxeoGroup.getParentGroups()
219                      .stream()
220                      .filter(pg -> !allGroups.contains(pg))
221                      .map(userManager::getGroup)
222                      .forEach(queue::add);
223        }
224
225        return allGroups;
226    }
227
228}