blob: c71bc90d38506fc2cc65abc3a211d64030c672f6 [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.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Arrays;
import com.google.common.annotations.VisibleForTesting;
import org.apache.log4j.Logger;
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 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,List<String>> user2GroupListMap = new HashMap<String,List<String>>();
private Map<String,List<String>> internalUser2GroupListMap = new HashMap<String,List<String>>();
private Map<String,String> groupId2groupNameMap = new HashMap<String,String>() ;
private int minimumUserId = 0 ;
private int minimumGroupId = 0 ;
private long passwordFileModifiedAt = 0 ;
private long groupFileModifiedAt = 0 ;
public static void main(String[] args) throws Throwable {
UnixUserGroupBuilder ugbuilder = new UnixUserGroupBuilder() ;
ugbuilder.init();
ugbuilder.print();
}
public UnixUserGroupBuilder() {
minimumUserId = Integer.parseInt(config.getMinUserId()) ;
minimumGroupId = Integer.parseInt(config.getMinGroupId()) ;
LOG.debug("Minimum UserId: " + minimumUserId + ", minimum GroupId: " + minimumGroupId) ;
timeout = config.getUpdateMillisMin() ;
enumerateGroupMembers = config.isGroupEnumerateEnabled();
if (!config.getUnixBackend().equalsIgnoreCase(BACKEND_PASSWD)) {
useNss = true;
} else {
LOG.warn("DEPRECATED: Unix backend is configured to use /etc/passwd and /etc/group files directly " +
"instead of standard system mechanisms.");
}
}
@Override
public void init() throws Throwable {
buildUserGroupInfo() ;
}
@Override
public boolean isChanged() {
if (useNss)
return System.currentTimeMillis() - lastUpdateTime > timeout ;
long TempPasswordFileModifiedAt = new File(UNIX_USER_PASSWORD_FILE).lastModified() ;
if (passwordFileModifiedAt != TempPasswordFileModifiedAt) {
return true ;
}
long TempGroupFileModifiedAt = new File(UNIX_GROUP_FILE).lastModified() ;
if (groupFileModifiedAt != TempGroupFileModifiedAt) {
return true ;
}
return false ;
}
@Override
public void updateSink(UserGroupSink sink) throws Throwable {
buildUserGroupInfo() ;
for (Map.Entry<String, List<String>> entry : user2GroupListMap.entrySet()) {
String user = entry.getKey();
List<String> groups = entry.getValue();
try{
sink.addOrUpdateUser(user, groups);
}catch (Throwable t) {
LOG.error("sink.addOrUpdateUser failed with exception: " + t.getMessage()
+ ", for user: " + user
+ ", groups: " + groups);
}
}
}
private void buildUserGroupInfo() throws Throwable {
user2GroupListMap = new HashMap<String,List<String>>();
groupId2groupNameMap = new HashMap<String, String>();
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);
}
lastUpdateTime = System.currentTimeMillis() ;
if (LOG.isDebugEnabled()) {
print() ;
}
}
private void print() {
for(String user : user2GroupListMap.keySet()) {
LOG.debug("USER:" + user) ;
List<String> groups = user2GroupListMap.get(user) ;
if (groups != null) {
for(String group : groups) {
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(UNIX_USER_PASSWORD_FILE);
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 < 3) {
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) {
List<String> groupList = new ArrayList<String>();
groupList.add(groupName);
// do we already know about this use's membership to other groups? If so add those, too
if (internalUser2GroupListMap.containsKey(userName)) {
List<String> map = internalUser2GroupListMap.get(userName);
// there could be duplicates
map.remove(groupName);
groupList.addAll(map);
}
user2GroupListMap.put(userName, groupList);
} 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 {
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;
LOG.debug("Start drill down group members");
for (Map.Entry<String, List<String>> entry : internalUser2GroupListMap.entrySet()) {
// skip users we already now about
if (user2GroupListMap.containsKey(entry.getKey()))
continue;
LOG.debug("Enumerating user " + entry.getKey());
int numUserId = -1;
try {
numUserId = Integer.parseInt(userName2uid.get(entry.getKey()));
} 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 " + entry.getKey()});
try {
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
line = reader.readLine();
} finally {
reader.close();
}
LOG.debug("id -G returned " + line);
if (line == null || line.trim().isEmpty()) {
LOG.warn("User " + entry.getKey() + " 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)
allowedGroups.add(groupName);
}
user2GroupListMap.put(entry.getKey(), allowedGroups);
}
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 < 2)
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);
if (groupMembers != null && !groupMembers.trim().isEmpty()) {
for (String user : groupMembers.split(",")) {
List<String> groupList = internalUser2GroupListMap.get(user);
if (groupList == null) {
groupList = new ArrayList<String>();
internalUser2GroupListMap.put(user, groupList);
}
if (!groupList.contains(groupName)) {
groupList.add(groupName);
}
}
}
}
private void buildUnixGroupList(String allGroupsCmd, String groupCmd, boolean useGid) throws Throwable {
LOG.debug("Start enumerating groups");
BufferedReader reader = null;
try {
if (!useNss) {
File file = new File(UNIX_GROUP_FILE);
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();
}
LOG.debug("End enumerating group");
if (!useNss)
return;
if (enumerateGroupMembers) {
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()) {
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()};
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();
}
LOG.debug("bash -c " + command + " for group " + group + " returned " + line);
parseMembers(line);
}
LOG.debug("End enumerating group members");
}
if (config.getEnumerateGroups() != null) {
String line = null;
String[] groups = config.getEnumerateGroups().split(",");
LOG.debug("Adding extra groups");
for (String group : groups) {
String command = String.format(groupCmd, group);
String[] cmd = new String[]{"bash", "-c", command + " '" + group + "'"};
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();
}
LOG.debug("bash -c " + command + " for group " + group + " returned " + line);
parseMembers(line);
}
LOG.debug("Done adding extra groups");
}
}
@VisibleForTesting
Map<String,List<String>> getUser2GroupListMap() {
return user2GroupListMap;
}
@VisibleForTesting
Map<String,String> getGroupId2groupNameMap() {
return groupId2groupNameMap;
}
}