blob: 597cbf8c01d9d05e483d1fd098f498f3bb6ddf8c [file] [log] [blame]
/*
* 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();
}
}