| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| |
| package org.apache.ranger.unixusersync.process; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.InputStreamReader; |
| import java.nio.charset.StandardCharsets; |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.HashSet; |
| import java.util.HashMap; |
| import java.util.Date; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.HashBasedTable; |
| import com.google.common.collect.Table; |
| import org.apache.log4j.Logger; |
| import org.apache.ranger.ugsyncutil.model.UgsyncAuditInfo; |
| import org.apache.ranger.ugsyncutil.model.UnixSyncSourceInfo; |
| import org.apache.ranger.unixusersync.config.UserGroupSyncConfig; |
| import org.apache.ranger.usergroupsync.UserGroupSink; |
| import org.apache.ranger.usergroupsync.UserGroupSource; |
| |
| public class UnixUserGroupBuilder implements UserGroupSource { |
| |
| private static final Logger LOG = Logger.getLogger(UnixUserGroupBuilder.class); |
| private final static String OS = System.getProperty("os.name"); |
| |
| // kept for legacy support |
| //public static final String UNIX_USER_PASSWORD_FILE = "/etc/passwd"; |
| //public static final String UNIX_GROUP_FILE = "/etc/group"; |
| |
| /** Shell commands to get users and groups */ |
| static final String LINUX_GET_ALL_USERS_CMD = "getent passwd"; |
| static final String LINUX_GET_ALL_GROUPS_CMD = "getent group"; |
| static final String LINUX_GET_GROUP_CMD = "getent group %s"; |
| |
| // mainly for testing purposes |
| // there might be a better way |
| static final String MAC_GET_ALL_USERS_CMD = "dscl . -readall /Users UniqueID PrimaryGroupID | " + |
| "awk 'BEGIN { OFS = \":\"; ORS=\"\\n\"; i=0;}" + |
| "/RecordName: / {name = $2;i = 0;}/PrimaryGroupID: / {gid = $2;}" + |
| "/^ / {if (i == 0) { i++; name = $1;}}" + |
| "/UniqueID: / {uid = $2;print name, \"*\", gid, uid;}'"; |
| static final String MAC_GET_ALL_GROUPS_CMD = "dscl . -list /Groups PrimaryGroupID | " + |
| "awk -v OFS=\":\" '{print $1, \"*\", $2, \"\"}'"; |
| static final String MAC_GET_GROUP_CMD = "dscl . -read /Groups/%1$s | paste -d, -s - | sed -e 's/:/|/g' | " + |
| "awk -v OFS=\":\" -v ORS=\"\\n\" -F, '{print \"%1$s\",\"*\",$6,$4}' | " + |
| "sed -e 's/:[^:]*| /:/g' | sed -e 's/ /,/g'"; |
| |
| static final String BACKEND_PASSWD = "passwd"; |
| |
| private boolean isUpdateSinkSucc = true; |
| private boolean enumerateGroupMembers = false; |
| private boolean useNss = false; |
| |
| private long lastUpdateTime = 0; // Last time maps were updated |
| private long timeout = 0; |
| |
| private UserGroupSyncConfig config = UserGroupSyncConfig.getInstance(); |
| private Map<String,String> groupId2groupNameMap; |
| private Map<String, Map<String, String>> sourceUsers; // Stores username and attr name & value pairs |
| private Map<String, Map<String, String>> sourceGroups; // Stores groupname and attr name & value pairs |
| private Map<String, Set<String>> sourceGroupUsers; |
| private Table<String, String, String> groupUserTable; // groupname, username, group id |
| private int minimumUserId = 0; |
| private int minimumGroupId = 0; |
| private String unixPasswordFile; |
| private String unixGroupFile; |
| |
| private long passwordFileModifiedAt = 0; |
| private long groupFileModifiedAt = 0; |
| private UgsyncAuditInfo ugsyncAuditInfo; |
| private UnixSyncSourceInfo unixSyncSourceInfo; |
| private boolean isStartupFlag = false; |
| Set<String> allGroups = new HashSet<>(); |
| |
| |
| public static void main(String[] args) throws Throwable { |
| UnixUserGroupBuilder ugbuilder = new UnixUserGroupBuilder(); |
| ugbuilder.init(); |
| ugbuilder.print(); |
| } |
| |
| public UnixUserGroupBuilder() { |
| isStartupFlag = true; |
| minimumUserId = Integer.parseInt(config.getMinUserId()); |
| minimumGroupId = Integer.parseInt(config.getMinGroupId()); |
| unixPasswordFile = config.getUnixPasswordFile(); |
| unixGroupFile = config.getUnixGroupFile(); |
| ugsyncAuditInfo = new UgsyncAuditInfo(); |
| unixSyncSourceInfo = new UnixSyncSourceInfo(); |
| ugsyncAuditInfo.setSyncSource("Unix"); |
| ugsyncAuditInfo.setUnixSyncSourceInfo(unixSyncSourceInfo); |
| unixSyncSourceInfo.setFileName(unixPasswordFile); |
| unixSyncSourceInfo.setMinUserId(config.getMinUserId()); |
| unixSyncSourceInfo.setMinGroupId(config.getMinGroupId()); |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Minimum UserId: " + minimumUserId + ", minimum GroupId: " + minimumGroupId); |
| } |
| |
| timeout = config.getUpdateMillisMin(); |
| enumerateGroupMembers = config.isGroupEnumerateEnabled(); |
| |
| if (!config.getUnixBackend().equalsIgnoreCase(BACKEND_PASSWD)) { |
| useNss = true; |
| unixSyncSourceInfo.setUnixBackend("nss"); |
| } else { |
| LOG.warn("DEPRECATED: Unix backend is configured to use /etc/passwd and /etc/group files directly " + |
| "instead of standard system mechanisms."); |
| unixSyncSourceInfo.setUnixBackend(BACKEND_PASSWD); |
| } |
| |
| } |
| |
| @Override |
| public void init() throws Throwable { |
| buildUserGroupInfo(); |
| } |
| |
| @Override |
| public boolean isChanged() { |
| // If previous update to Ranger admin fails, |
| // we want to retry the sync process even if there are no changes to the sync files |
| if (!isUpdateSinkSucc) { |
| LOG.info("Previous updateSink failed and hence retry!!"); |
| return true; |
| } |
| |
| if (useNss) |
| return System.currentTimeMillis() - lastUpdateTime > timeout; |
| |
| long TempPasswordFileModifiedAt = new File(unixPasswordFile).lastModified(); |
| if (passwordFileModifiedAt != TempPasswordFileModifiedAt) { |
| return true; |
| } |
| |
| long TempGroupFileModifiedAt = new File(unixGroupFile).lastModified(); |
| if (groupFileModifiedAt != TempGroupFileModifiedAt) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| |
| @Override |
| public void updateSink(UserGroupSink sink) throws Throwable { |
| DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| Date lastModifiedTime = new Date(passwordFileModifiedAt); |
| Date syncTime = new Date(System.currentTimeMillis()); |
| unixSyncSourceInfo.setLastModified(formatter.format(lastModifiedTime)); |
| unixSyncSourceInfo.setSyncTime(formatter.format(syncTime)); |
| isUpdateSinkSucc = true; |
| if (isChanged() || isStartupFlag) { |
| buildUserGroupInfo(); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Users = " + sourceUsers.keySet()); |
| LOG.debug("Groups = " + sourceGroups.keySet()); |
| LOG.debug("GroupUsers = " + sourceGroupUsers.keySet()); |
| } |
| |
| try { |
| sink.addOrUpdateUsersGroups(sourceGroups, sourceUsers, sourceGroupUsers); |
| } catch (Throwable t) { |
| LOG.error("Failed to update ranger admin. Will retry in next sync cycle!!", t); |
| isUpdateSinkSucc = false; |
| } |
| } |
| try { |
| sink.postUserGroupAuditInfo(ugsyncAuditInfo); |
| } catch (Throwable t) { |
| LOG.error("sink.postUserGroupAuditInfo failed with exception: ", t); |
| } |
| isStartupFlag = false; |
| } |
| |
| |
| private void buildUserGroupInfo() throws Throwable { |
| groupId2groupNameMap = new HashMap<String, String>(); |
| sourceUsers = new HashMap<>(); |
| sourceGroups = new HashMap<>(); |
| sourceGroupUsers = new HashMap<>(); |
| groupUserTable = HashBasedTable.create(); |
| allGroups = new HashSet<>(); |
| |
| if (OS.startsWith("Mac")) { |
| buildUnixGroupList(MAC_GET_ALL_GROUPS_CMD, MAC_GET_GROUP_CMD, false); |
| buildUnixUserList(MAC_GET_ALL_USERS_CMD); |
| } else { |
| if (!OS.startsWith("Linux")) { |
| LOG.warn("Platform not recognized assuming Linux compatible"); |
| } |
| buildUnixGroupList(LINUX_GET_ALL_GROUPS_CMD, LINUX_GET_GROUP_CMD, true); |
| buildUnixUserList(LINUX_GET_ALL_USERS_CMD); |
| } |
| |
| Iterator<String> groupUserTableIterator = groupUserTable.rowKeySet().iterator(); |
| while (groupUserTableIterator.hasNext()) { |
| String groupName = groupUserTableIterator.next(); |
| Map<String,String> groupUsersMap = groupUserTable.row(groupName); |
| Set<String> userSet = new HashSet<String>(); |
| for(String userName : groupUsersMap.keySet()){ |
| //String transformUserName = userNameTransform(entry.getKey()); |
| if (sourceUsers.containsKey(userName)) { |
| userSet.add(userName); |
| } |
| } |
| sourceGroupUsers.put(groupName, userSet); |
| } |
| |
| lastUpdateTime = System.currentTimeMillis(); |
| |
| if (LOG.isDebugEnabled()) { |
| print(); |
| } |
| } |
| |
| private void print() { |
| for(String user : sourceUsers.keySet()) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("USER:" + user); |
| } |
| Set<String> groups = groupUserTable.column(user).keySet(); |
| if (groups != null) { |
| for(String group : groups) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("\tGROUP: " + group); |
| } |
| } |
| } |
| } |
| } |
| |
| private void buildUnixUserList(String command) throws Throwable { |
| BufferedReader reader = null; |
| Map<String, String> userName2uid = new HashMap<String, String>(); |
| |
| try { |
| if (!useNss) { |
| File file = new File(unixPasswordFile); |
| passwordFileModifiedAt = file.lastModified(); |
| FileInputStream fis = new FileInputStream(file); |
| reader = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); |
| } else { |
| Process process = Runtime.getRuntime().exec( |
| new String[]{"bash", "-c", command}); |
| |
| reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); |
| } |
| |
| String line = null; |
| |
| while ((line = reader.readLine()) != null) { |
| if (line.trim().isEmpty()) |
| continue; |
| |
| String[] tokens = line.split(":"); |
| |
| int len = tokens.length; |
| |
| if (len < 4) { |
| LOG.warn("Unable to parse: " + line); |
| continue; |
| } |
| |
| String userName = null; |
| String userId = null; |
| String groupId = null; |
| |
| try { |
| userName = tokens[0]; |
| userId = tokens[2]; |
| groupId = tokens[3]; |
| } |
| catch(ArrayIndexOutOfBoundsException aiobe) { |
| LOG.warn("Ignoring line - [" + line + "]: Unable to parse line for getting user information", aiobe); |
| continue; |
| } |
| |
| int numUserId = -1; |
| |
| try { |
| numUserId = Integer.parseInt(userId); |
| } catch (NumberFormatException nfe) { |
| LOG.warn("Unix UserId: [" + userId + "]: can not be parsed as valid int. considering as -1.", nfe); |
| numUserId = -1; |
| } |
| |
| if (numUserId >= minimumUserId) { |
| userName2uid.put(userName, userId); |
| String groupName = groupId2groupNameMap.get(groupId); |
| if (groupName != null) { |
| Map<String, String> userAttrMap = new HashMap<>(); |
| userAttrMap.put("original_name", userName); |
| userAttrMap.put("full_name", userName); |
| sourceUsers.put(userName, userAttrMap); |
| groupUserTable.put(groupName, userName, groupId); |
| } else { |
| // we are ignoring the possibility that this user was present in /etc/groups. |
| LOG.warn("Group Name could not be found for group id: [" + groupId + "]. Skipping adding user [" + userName + "] with id [" + userId + "]."); |
| } |
| } else { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Skipping user [" + userName + "] since its userid [" + userId + "] is less than minuserid limit [" + minimumUserId + "]."); |
| } |
| } |
| } |
| } finally { |
| if (reader != null) |
| reader.close(); |
| } |
| |
| if (!useNss) |
| return; |
| |
| // this does a reverse check as not all users might be listed in getent passwd |
| if (enumerateGroupMembers) { |
| String line = null; |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Start drill down group members"); |
| } |
| for (String userName : groupUserTable.columnKeySet()) { |
| // skip users we already now about |
| if (sourceUsers.containsKey(userName)) |
| continue; |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Enumerating user " + userName); |
| } |
| |
| int numUserId = -1; |
| try { |
| numUserId = Integer.parseInt(userName2uid.get(userName)); |
| } catch (NumberFormatException nfe) { |
| numUserId = -1; |
| } |
| |
| // if a user comes from an external group we might not have a uid |
| if (numUserId < minimumUserId && numUserId != -1) |
| continue; |
| |
| |
| // "id" is same across Linux / BSD / MacOSX |
| // gids are used as id might return groups with spaces, ie "domain users" |
| Process process = Runtime.getRuntime().exec( |
| new String[]{"bash", "-c", "id -G " + userName}); |
| |
| try { |
| reader = new BufferedReader(new InputStreamReader(process.getInputStream())); |
| line = reader.readLine(); |
| } finally { |
| reader.close(); |
| } |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("id -G returned " + line); |
| } |
| |
| if (line == null || line.trim().isEmpty()) { |
| LOG.warn("User " + userName + " could not be resolved"); |
| continue; |
| } |
| |
| String[] gids = line.split(" "); |
| |
| // check if all groups returned by id are visible to ranger |
| //ArrayList<String> allowedGroups = new ArrayList<String>(); |
| for (String gid : gids) { |
| int numGroupId = Integer.parseInt(gid); |
| if (numGroupId < minimumGroupId) |
| continue; |
| |
| String groupName = groupId2groupNameMap.get(gid); |
| if (groupName != null) { |
| groupUserTable.put(groupName, userName, gid); |
| //allowedGroups.add(groupName); |
| } |
| } |
| Map<String, String> userAttrMap = new HashMap<>(); |
| userAttrMap.put("original_name", userName); |
| userAttrMap.put("full_name", userName); |
| sourceUsers.put(userName, userAttrMap); |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("End drill down group members"); |
| } |
| } |
| } |
| |
| private void parseMembers(String line) { |
| if (line == null || line.isEmpty()) |
| return; |
| |
| String[] tokens = line.split(":"); |
| |
| if (tokens.length < 3) |
| return; |
| |
| String groupName = tokens[0]; |
| String groupId = tokens[2]; |
| String groupMembers = null; |
| |
| if (tokens.length > 3) |
| groupMembers = tokens[3]; |
| |
| if (groupId2groupNameMap.containsKey(groupId)) { |
| groupId2groupNameMap.remove(groupId); |
| } |
| |
| int numGroupId = Integer.parseInt(groupId); |
| if (numGroupId < minimumGroupId) |
| return; |
| |
| groupId2groupNameMap.put(groupId, groupName); |
| Map<String, String> groupAttrMap = new HashMap<>(); |
| groupAttrMap.put("original_name", groupName); |
| groupAttrMap.put("full_name", groupName); |
| sourceGroups.put(groupName, groupAttrMap); |
| |
| if (groupMembers != null && !groupMembers.trim().isEmpty()) { |
| for (String user : groupMembers.split(",")) { |
| groupUserTable.put(groupName, user, groupId); |
| } |
| } |
| } |
| |
| private void buildUnixGroupList(String allGroupsCmd, String groupCmd, boolean useGid) throws Throwable { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Start enumerating groups"); |
| } |
| BufferedReader reader = null; |
| |
| try { |
| if (!useNss) { |
| File file = new File(unixGroupFile); |
| groupFileModifiedAt = file.lastModified(); |
| FileInputStream fis = new FileInputStream(file); |
| reader = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); |
| } else { |
| Process process = Runtime.getRuntime().exec( |
| new String[]{"bash", "-c", allGroupsCmd}); |
| reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); |
| } |
| |
| String line = null; |
| |
| while ((line = reader.readLine()) != null) { |
| if (line.trim().isEmpty()) |
| continue; |
| |
| parseMembers(line); |
| } |
| } finally { |
| if (reader != null) |
| reader.close(); |
| } |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("End enumerating group"); |
| } |
| |
| if (!useNss) |
| return; |
| |
| if (enumerateGroupMembers) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Start enumerating group members"); |
| } |
| String line = null; |
| Map<String,String> copy = new HashMap<String, String>(groupId2groupNameMap); |
| |
| for (Map.Entry<String, String> group : copy.entrySet()) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Enumerating group: " + group.getValue() + " GID(" + group.getKey() + ")"); |
| } |
| |
| String command; |
| if (useGid) { |
| command = String.format(groupCmd, group.getKey()); |
| } else { |
| command = String.format(groupCmd, group.getValue()); |
| } |
| |
| String[] cmd = new String[]{"bash", "-c", command + " " + group.getKey()}; |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Executing: " + Arrays.toString(cmd)); |
| } |
| |
| try { |
| Process process = Runtime.getRuntime().exec(cmd); |
| reader = new BufferedReader(new InputStreamReader(process.getInputStream())); |
| line = reader.readLine(); |
| } finally { |
| if (reader != null) |
| reader.close(); |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("bash -c " + command + " for group " + group + " returned " + line); |
| } |
| |
| parseMembers(line); |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("End enumerating group members"); |
| } |
| } |
| |
| if (config.getEnumerateGroups() != null) { |
| String line = null; |
| String[] groups = config.getEnumerateGroups().split(","); |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Adding extra groups"); |
| } |
| |
| for (String group : groups) { |
| String command = String.format(groupCmd, group); |
| String[] cmd = new String[]{"bash", "-c", command + " '" + group + "'"}; |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Executing: " + Arrays.toString(cmd)); |
| } |
| |
| try { |
| Process process = Runtime.getRuntime().exec(cmd); |
| reader = new BufferedReader(new InputStreamReader(process.getInputStream())); |
| line = reader.readLine(); |
| } finally { |
| if (reader != null) |
| reader.close(); |
| } |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("bash -c " + command + " for group " + group + " returned " + line); |
| } |
| |
| parseMembers(line); |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Done adding extra groups"); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| Map<String, Set<String>> getGroupUserListMap() { |
| return sourceGroupUsers; |
| } |
| |
| @VisibleForTesting |
| Map<String,String> getGroupId2groupNameMap() { |
| return groupId2groupNameMap; |
| } |
| |
| @VisibleForTesting |
| Set<String> getUsers() { |
| return sourceUsers.keySet(); |
| } |
| |
| } |