/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2016 Adobe * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package com.adobe.acs.commons.exporters.impl.users; import com.day.cq.commons.jcr.JcrConstants; import com.day.text.csv.Csv; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.sling.SlingServlet; import org.apache.jackrabbit.api.security.user.Authorizable; import org.apache.jackrabbit.api.security.user.Group; import org.apache.jackrabbit.api.security.user.UserManager; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.*; import org.apache.sling.api.servlets.SlingSafeMethodsServlet; import org.apache.sling.commons.json.JSONException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.RepositoryException; import javax.jcr.query.Query; import javax.servlet.ServletException; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static com.adobe.acs.commons.exporters.impl.users.Constants.*; @SlingServlet( label = "ACS AEM Commons - Users to CSV - Export Servlet", methods = {"GET"}, resourceTypes = {"acs-commons/components/utilities/exporters/users-to-csv"}, selectors = {"export"}, extensions = {"csv"} ) public class UsersExportServlet extends SlingSafeMethodsServlet { private static final Logger log = LoggerFactory.getLogger(UsersExportServlet.class); private static final String QUERY = "SELECT * FROM [rep:User] WHERE ISDESCENDANTNODE([/home/users]) ORDER BY [rep:principalName]"; private static final String GROUP_DELIMITER = "|"; /** * Generates a CSV file representing the User Data. * * @param request the Sling HTTP Request object * @param response the Sling HTTP Response object * @throws IOException * @throws ServletException */ public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException, ServletException { response.setContentType("text/csv"); response.setCharacterEncoding("UTF-8"); final Parameters parameters; try { parameters = new Parameters(request); } catch (JSONException e) { throw new ServletException(e); } log.debug("Users to CSV Export Parameters: {}", parameters.toString()); final Csv csv = new Csv(); final Writer writer = response.getWriter(); csv.writeInit(writer); final Iterator<Resource> resources = request.getResourceResolver().findResources(QUERY, Query.JCR_SQL2); // Using a HashMap to satisfy issue with duplicate results in AEM 6.1 GA HashMap<String, CsvUser> csvUsers = new LinkedHashMap<String, CsvUser>(); while (resources.hasNext()) { try { Resource resource = resources.next(); CsvUser csvUser = new CsvUser(resource); if (!csvUsers.containsKey(csvUser.getPath()) && checkGroups(parameters.getGroups(), parameters.getGroupFilter(), csvUser)) { csvUsers.put(csvUser.getPath(), csvUser); } } catch (RepositoryException e) { log.error("Unable to extract a user from resource.", e); } } List<String> columns = new ArrayList<String>(); columns.add("Path"); columns.add("User ID"); columns.add("First Name"); columns.add("Last Name"); columns.add("E-mail Address"); columns.add("Created Date"); columns.add("Last Modified Date"); for (String customProperty : parameters.getCustomProperties()) { columns.add(customProperty); } columns.add("All Groups"); columns.add("Direct Groups"); columns.add("Indirect Groups"); csv.writeRow(columns.toArray(new String[columns.size()])); for (final CsvUser csvUser : csvUsers.values()) { List<String> values = new ArrayList<String>(); try { values.add(csvUser.getPath()); values.add(csvUser.getID()); values.add(csvUser.getFirstName()); values.add(csvUser.getLastName()); values.add(csvUser.getEmail()); values.add(csvUser.getCreatedDate()); values.add(csvUser.getLastModifiedDate()); for (String customProperty : parameters.getCustomProperties()) { values.add(csvUser.getCustomProperty(customProperty)); } values.add(StringUtils.join(csvUser.getAllGroups(), GROUP_DELIMITER)); values.add(StringUtils.join(csvUser.getDeclaredGroups(), GROUP_DELIMITER)); values.add(StringUtils.join(csvUser.getTransitiveGroups(), GROUP_DELIMITER)); csv.writeRow(values.toArray(new String[values.size()])); } catch (RepositoryException e) { log.error("Unable to export user to CSV report", e); } } csv.close(); } /** * Determines if the user should be included based on the specified group filter type, and requested groups. * * @param groups the groups * @param groupFilter the groupFilter * @param csvUser the user * @return true if the user should be included. */ protected boolean checkGroups(String[] groups, String groupFilter, CsvUser csvUser) throws RepositoryException { log.debug("Group Filter: {}", groupFilter); if (!ArrayUtils.isEmpty(groups)) { if (GROUP_FILTER_DIRECT.equals(groupFilter) && csvUser.isInDirectGroup(groups)) { log.debug("Adding [ {} ] via [ Direct ] membership", csvUser.getID()); return true; } else if (GROUP_FILTER_INDIRECT.equals(groupFilter) && csvUser.isInIndirectGroup(groups)) { log.debug("Adding [ {} ] via [ Indirect ] membership", csvUser.getID()); return true; } else if (GROUP_FILTER_BOTH.equals(groupFilter) && (csvUser.isInDirectGroup(groups) || csvUser.isInIndirectGroup(groups))) { log.debug("Adding [ {} ] via [ Direct OR Indirect ] membership", csvUser.getID()); return true; } return false; } log.debug("Adding [ {} ] as no groups were specified to specify membership filtering.", csvUser.getID()); return true; } /** * Internal class representing a user that will be exported in CSV format. */ protected static class CsvUser { private final ValueMap properties; private Set<String> declaredGroups = new LinkedHashSet<String>(); private Set<String> transitiveGroups = new LinkedHashSet<String>(); private Set<String> allGroups = new LinkedHashSet<String>(); private Authorizable authorizable; private String email; private String firstName; private String lastName; private Calendar createdDate; private Calendar lastModifiedDate; private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); public CsvUser(Resource resource) throws RepositoryException { if (resource == null) { throw new IllegalArgumentException("Authorizable object cannot be null"); } final UserManager userManager = resource.adaptTo(UserManager.class); this.properties = resource.getValueMap(); this.authorizable = userManager.getAuthorizableByPath(resource.getPath()); this.declaredGroups = getGroupIds(authorizable.declaredMemberOf()); this.transitiveGroups = getGroupIds(authorizable.memberOf()); this.allGroups.addAll(this.transitiveGroups); this.allGroups.addAll(this.declaredGroups); this.transitiveGroups.removeAll(this.declaredGroups); this.firstName = properties.get("profile/givenName", ""); this.lastName = properties.get("profile/familyName", ""); this.email = properties.get("profile/email", ""); this.createdDate = properties.get(JcrConstants.JCR_CREATED, Calendar.class); this.lastModifiedDate = properties.get("cq:lastModified", Calendar.class); } public List<String> getDeclaredGroups() { return new ArrayList<String>(declaredGroups); } public List<String> getTransitiveGroups() { return new ArrayList<String>(transitiveGroups); } public List<String> getAllGroups() { return new ArrayList<String>(allGroups); } public String getPath() throws RepositoryException { return authorizable.getPath(); } public String getID() throws RepositoryException { return authorizable.getID(); } private Set<String> getGroupIds(Iterator<Group> groups) throws RepositoryException { final List<String> groupIDs = new ArrayList<String>(); while (groups.hasNext()) { groupIDs.add(groups.next().getID()); } Collections.sort(groupIDs); return new LinkedHashSet<String>(groupIDs); } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getCreatedDate() { if (createdDate != null) { return sdf.format(createdDate.getTime()); } else { return ""; } } public String getLastModifiedDate() { if (lastModifiedDate != null) { return sdf.format(lastModifiedDate.getTime()); } else { return ""; } } public String getEmail() { return email; } public boolean isInDirectGroup(String... groups) { return CollectionUtils.containsAny(this.getDeclaredGroups(), Arrays.asList(groups)); } public boolean isInIndirectGroup(String... groups) { return CollectionUtils.containsAny(this.getTransitiveGroups(), Arrays.asList(groups)); } public String getCustomProperty(String customProperty) { return properties.get(customProperty, ""); } } }