#! /usr/bin/env python
# Copyright (C) 2004 Tresys Technology, LLC
# see file 'COPYING' for use and warranty information
#
# genhomedircon - this script is used to generate file context
# configuration entries for user home directories based on their
# default roles and is run when building the policy. Specifically, we
# replace HOME_ROOT, HOME_DIR, and ROLE macros in .fc files with
# generic and user-specific values.
#
# Based off original script by Dan Walsh, <dwalsh@redhat.com>
#
# ASSUMPTIONS:
#
# The file CONTEXTDIR/files/homedir_template exists.  This file is used to
# set up the home directory context for each real user.
# 
# If a user has more than one role in CONTEXTDIR/local.users, genhomedircon uses
#  the first role in the list.
#
# If a user is not listed in CONTEXTDIR/local.users, he will default to user_u, role user
#
# "Real" users (as opposed to system users) are those whose UID is greater than
#  or equal STARTING_UID (usually 500) and whose login is not a member of
#  EXCLUDE_LOGINS.  Users who are explicitly defined in CONTEXTDIR/local.users
#  are always "real" (including root, in the default configuration).
#
#  
# Old ASSUMPTIONS:
#
# If a user has more than one role in FILECONTEXTDIR/users, genhomedircon uses
#  the first role in the list.
#
# If a user is not listed in FILECONTEXTDIR/users, genhomedircon assumes that
#  the user's home dir will be found in one of the HOME_ROOTs.
#
# "Real" users (as opposed to system users) are those whose UID is greater than
#  or equal STARTING_UID (usually 500) and whose login is not a member of
#  EXCLUDE_LOGINS.  Users who are explicitly defined in FILECONTEXTDIR/users
#  are always "real" (including root, in the default configuration).
#

import commands, sys, os, pwd, string, getopt

EXCLUDE_LOGINS=["/sbin/nologin", "/bin/false"]

def getStartingUID():
    rc=commands.getstatusoutput("grep ^UID_MIN /etc/login.defs | tail -1")
    if rc[0]==0:
        return int(rc[1].split()[-1])
    return 500

#############################################################################
#
# This section is just for backwards compatability
#
#############################################################################
def getPrefixes():
	ulist = pwd.getpwall()
        STARTING_UID=getStartingUID()
	prefixes = {}
	for u in ulist:
		if u[2] >= STARTING_UID and \
		   not u[6] in EXCLUDE_LOGINS and \
		   u[5] != "/" and \
		   string.count(u[5], "/") > 1:
			prefix = u[5][:string.rfind(u[5], "/")]
			if not prefixes.has_key(prefix):
				prefixes[prefix] = ""
	return prefixes
 
def getUsers(filecontextdir):
	rc = commands.getstatusoutput("grep ^user %s/users" % filecontextdir)
	udict = {}
	if rc[0] == 0:
		ulist = rc[1].strip().split("\n")
		for u in ulist:
			user = u.split()
			try:
				if user[1] == "user_u" or user[1] == "system_u":
					continue
				# !!! chooses first role in the list to use in the file context !!!
				role = user[3]
				if role == "{":
					role = user[4]
				role = role.split("_r")[0]
				home = pwd.getpwnam(user[1])[5]
				if home == "/":
					continue
				prefs = {}
				prefs["role"] = role
				prefs["home"] = home
				udict[user[1]] = prefs
			except KeyError:
				sys.stderr.write("The user \"%s\" is not present in the passwd file, skipping...\n" % (user[1],))
	return udict

def update(filecontext, user, prefs):
	rc=commands.getstatusoutput("grep -h '^HOME_DIR' %s | grep -v vmware | sed -e 's|HOME_DIR|%s|' -e 's/ROLE/%s/' -e 's/system_u/%s/'" % (filecontext, prefs["home"], prefs["role"], user))
	if rc[0] == 0:
		print rc[1]
	else:
		errorExit(string.join("grep/sed error ", rc[1]))
	return rc

def oldgenhomedircon(filecontextdir, filecontext):
    sys.stderr.write("Using genhomedircon in this fashion is supported for backwards compatability\n")
    sys.stderr.write("Please update to the latest policy\n")
    sys.stderr.flush()

    if os.path.isdir(filecontextdir) == 0:
        sys.stderr.write("New usage is the following\n")
        usage()

    prefixes = getPrefixes()
    
    rc=commands.getstatusoutput("grep -h '^HOME' /etc/default/useradd")
    if rc[0] == 0:
        homedir = rc[1].split("=")[1]
    else:
        sys.stderr.write("%s\n" % (rc[1],))
        sys.stderr.write("You do not have access to /etc/default/useradd, default /home\n")
        sys.stderr.flush()
        homedir = "/home"
		
		
    if not prefixes.has_key(homedir):
        prefixes[homedir] = ""

    # There may be a more elegant sed script to expand a macro to multiple lines, but this works
    sed_root = "h; s|^HOME_ROOT|%s|" % (string.join(prefixes.keys(), "|; p; g; s|^HOME_ROOT|"),)
    sed_dir = "h; s|^HOME_DIR|%s/[^/]+|; s|ROLE_|user_|" % (string.join(prefixes.keys(), "/[^/]+|; s|ROLE_|user_|; p; g; s|^HOME_DIR|"),)
    
    # Fill in HOME_ROOT, HOME_DIR, and ROLE for users not explicitly defined in /etc/security/selinux/src/policy/users
    rc=commands.getstatusoutput("sed -e \"/^HOME_ROOT/{%s}\" -e \"/^HOME_DIR/{%s}\" %s" % (sed_root, sed_dir, filecontext))
    if rc[0] == 0:
        print rc[1]
    else:
        errorExit(string.join("sed error ", rc[1]))
        
    users = getUsers(filecontextdir)
    print "\n#\n# User-specific file contexts\n#\n"

    # Fill in HOME and ROLE for users that are defined
    for u in users.keys():
        update(filecontext, u, users[u]) 

#############################################################################
#
# End of backwards compatability section
#
#############################################################################

def getDefaultHomeDir():
    rc=commands.getstatusoutput("grep ^HOME= /etc/default/useradd | tail -1")
    if rc[0]==0:
        return rc[1].split("=")[-1].strip()
    return "/home"

def getSELinuxType(directory):
    rc=commands.getstatusoutput("grep ^SELINUXTYPE= %s/config | tail -1" % directory)
    if rc[0]==0:
        return rc[1].split("=")[-1].strip()
    return "targeted"

def usage(error = ""):
    if error != "":
        sys.stderr.write("%s\n" % (error,))
    sys.stderr.write("Usage: %s [ -d selinuxdir ] [-n | --nopasswd] [-t selinuxtype ]\n" % sys.argv[0])
    sys.stderr.flush()
    sys.exit(1)
    
def warning(warning = ""):
    sys.stderr.write("%s\n" % warning)
    sys.stderr.flush()
    
def errorExit(error):
    sys.stderr.write("%s exiting for: " % sys.argv[0])
    sys.stderr.write("%s\n" % error)
    sys.stderr.flush()
    sys.exit(1)
    
class selinuxConfig:
    def __init__(self, selinuxdir="/etc/selinux", type="targeted", usepwd=1):
	self.type=type
        self.selinuxdir=selinuxdir +"/"
        self.contextdir="/contexts"
        self.filecontextdir=self.contextdir+"/files"
        self.usepwd=usepwd

    def getFileContextDir(self):
	    return self.selinuxdir+self.type+self.filecontextdir

    def getFileContextFile(self):
        return self.getFileContextDir()+"/file_contexts"
    
    def getContextDir(self):
	    return self.selinuxdir+self.type+self.contextdir

    def getHomeDirTemplate(self):
	    return self.getFileContextDir()+"/homedir_template"

    def getHomeRootContext(self, homedir):
	    rc=commands.getstatusoutput("grep HOME_ROOT  %s | sed -e \"s|^HOME_ROOT|%s|\"" % ( self.getHomeDirTemplate(), homedir))
	    if rc[0] == 0:
		    return rc[1]+"\n"
	    else:
		    errorExit(string.join("sed error ", rc[1]))

    def getUsersFile(self):
	    return self.selinuxdir+self.type+"/users/local.users"

    def getSystemUsersFile(self):
	    return self.selinuxdir+self.type+"/users/system.users"
	    
    def heading(self):
	ret = "\n#\n#\n# User-specific file contexts, generated via %s\n" % sys.argv[0]
	ret += "# edit %s to change file_context\n#\n#\n" % self.getUsersFile()
	return ret

    def getUsers(self):
        users=""
	rc = commands.getstatusoutput('grep "^user" %s' % self.getSystemUsersFile())
	if rc[0] == 0:
                users+=rc[1]+"\n"
	rc = commands.getstatusoutput("grep ^user %s" % self.getUsersFile())
	if rc[0] == 0:
                users+=rc[1]
	udict = {}
	prefs = {}
	if users != "":
		ulist = users.split("\n")
		for u in ulist:
			user = u.split()
			try:
				if len(user)==0 or user[1] == "user_u" or user[1] == "system_u":
					continue
				# !!! chooses first role in the list to use in the file context !!!
				role = user[3]
				if role == "{":
					role = user[4]
				role = role.split("_r")[0]
				home = pwd.getpwnam(user[1])[5]
				if home == "/":
					continue
				prefs = {}
				prefs["role"] = role
				prefs["home"] = home
				udict[user[1]] = prefs
			except KeyError:
				sys.stderr.write("The user \"%s\" is not present in the passwd file, skipping...\n" % (user[1],))
	return udict

    def getHomeDirContext(self, user, home, role):
	    ret="\n\n#\n# Context for user %s\n#\n\n" % user
            rc=commands.getstatusoutput("grep '^HOME_DIR' %s | sed -e 's|HOME_DIR|%s|' -e 's/ROLE/%s/' -e 's/system_u/%s/'" % (self.getHomeDirTemplate(), home, role, user))
	    return ret + rc[1] + "\n"

    def genHomeDirContext(self):
	users = self.getUsers()
	ret=""
	# Fill in HOME and ROLE for users that are defined
	for u in users.keys():
		ret += self.getHomeDirContext (u, users[u]["home"], users[u]["role"])
	return ret+"\n"

    def checkExists(self, home):
        return commands.getstatusoutput("grep -E '^%s[^[:alnum:]_-]' %s" % (home, self.getFileContextFile()))[0]

    def getHomeDirs(self):
        homedirs = []
        homedirs.append(getDefaultHomeDir())
        starting_uid=getStartingUID()
        if self.usepwd==0:
            return homedirs
        ulist = pwd.getpwall()
        for u in ulist:
            if u[2] >= starting_uid and \
                   not u[6] in EXCLUDE_LOGINS and \
                   u[5] != "/" and \
                   string.count(u[5], "/") > 1:
                homedir = u[5][:string.rfind(u[5], "/")]
                if not homedir in homedirs:
                    if self.checkExists(homedir)==0:
                        warning("%s is already defined in %s,\n%s will not create a new context." % (homedir, self.getFileContextFile(), sys.argv[0]))
                    else:
                        homedirs.append(homedir)
                    
        homedirs.sort()
        return homedirs
 
    def genoutput(self):
	ret= self.heading()
        for h in self.getHomeDirs():
            ret += self.getHomeDirContext ("user_u" , h+'/[^/]*', "user")
            ret += self.getHomeRootContext(h)
        ret += self.genHomeDirContext()
        return ret
    
    def printout(self):
        print self.genoutput()
        
    def write(self):
        try:
            fd = open(self.getFileContextDir()+"/file_contexts.homedirs", "w")
            fd.write(self.genoutput())
            fd.close()
        except IOError, error:
            sys.stderr.write("%s: %s\n" % ( sys.argv[0], error ))
            
            
        
#
# This script will generate home dir file context
# based off the homedir_template file, entries in the password file, and
#
try:
    usepwd=1
    directory="/etc/selinux"
    type=None
    gopts, cmds = getopt.getopt(sys.argv[1:], 'nd:t:', ['help',
                                                        'type=',
                                                        'nopasswd',
                                                        'dir='])
    for o,a in gopts:
        if o == '--type' or o == "-t":
            type=a
        if o == '--nopasswd'  or o == "-n":
            usepwd=0
        if o == '--dir'  or o == "-d":
            directory=a
        if o == '--help':
            usage()
        
            
    if type==None:
        type=getSELinuxType(directory)
        
    if len(cmds) == 2:
        oldgenhomedircon(cmds[0], cmds[1])
        sys.exit(0)
        
    if len(cmds) != 0:
        usage()
    selconf=selinuxConfig(directory, type, usepwd)
    selconf.write()
        
except getopt.error, error:
    errorExit(string.join("Options Error ", error))
    
except ValueError, error:
    errorExit(string.join("ValueError ", error))
except IndexError, error:
    errorExit("IndexError")
