# Copyright 2008 Fred Gansevles <fred@betterbe.com>
#
# This file is part of rdiff-backup.
#
# rdiff-backup is free software; you can redistribute it and/or modify
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# rdiff-backup is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with rdiff-backup; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA

from __future__ import generators
import C, metadata, re, rorpiter, rpath, log

try:
	from win32security import *
	import pywintypes
except ImportError:
	GROUP_SECURITY_INFORMATION = 0
	OWNER_SECURITY_INFORMATION = 0
	DACL_SECURITY_INFORMATION = 0

class ACL:
	flags = (GROUP_SECURITY_INFORMATION|
		 OWNER_SECURITY_INFORMATION|
		 DACL_SECURITY_INFORMATION)

	def __init__(self, index=()):
		self.__acl = ""
		self.index = index

	def get_indexpath(self): return self.index and '/'.join(self.index) or '.'

	def load_from_rp(self, rp, skip_inherit_only = True):
		self.index = rp.index
		try:
			sd = rp.conn.win32security. \
					GetNamedSecurityInfo(rp.path, SE_FILE_OBJECT, ACL.flags)
		except (OSError, IOError, pywintypes.error), exc:
			log.Log("Warning: unable to read ACL from %s: %s"
					% (repr(rp.path), exc), 4)
			return

		if skip_inherit_only:
			# skip the inherit_only aces
			acl = sd.GetSecurityDescriptorDacl()
			if acl:
				n = acl.GetAceCount()
				# traverse the ACL in reverse, so the indices stay correct
				while n:
					n -= 1
					ace_flags = acl.GetAce(n)[0][1]
					if ace_flags & INHERIT_ONLY_ACE:
						acl.DeleteAce(n)
			sd.SetSecurityDescriptorDacl(1, acl, 0)

			if ACL.flags & SACL_SECURITY_INFORMATION:
				acl = sd.GetSecurityDescriptorSacl()
				if acl:
					n = acl.GetAceCount()
					# traverse the ACL in reverse, so the indices stay correct
					while n:
						n -= 1
						ace_flags = acl.GetAce(n)[0][1]
						if ace_flags & INHERIT_ONLY_ACE:
							acl.DeleteAce(n)
					sd.SetSecurityDescriptorSacl(1, acl, 0)

		if not sd.GetSecurityDescriptorDacl():
				sd.SetSecurityDescriptorDacl(0, None, 0)
		if (ACL.flags & SACL_SECURITY_INFORMATION) and not \
				sd.GetSecurityDescriptorSacl():
			sd.SetSecurityDescriptorSacl(0, None, 0)

		try:
			self.__acl = \
				rp.conn.win32security. \
					ConvertSecurityDescriptorToStringSecurityDescriptor(sd,
					SDDL_REVISION_1, ACL.flags)
		except (OSError, IOError, pywintypes.error), exc:
			log.Log("Warning: unable to convert ACL from %s to string: %s"
					% (repr(rp.path), exc), 4)
			self.__acl = ''

	def clear_rp(self, rp):
		# not sure how to interpret this
		# I'll just clear all acl-s from rp.path
		try:
			sd = rp.conn.win32security. \
					GetNamedSecurityInfo(rp.path, SE_FILE_OBJECT, ACL.flags)
		except (OSError, IOError, pywintypes.error), exc:
			log.Log("Warning: unable to read ACL from %s for clearing: %s"
					% (repr(rp.path), exc), 4)

		acl = sd.GetSecurityDescriptorDacl()
		if acl:
			n = acl.GetAceCount()
			# traverse the ACL in reverse, so the indices stay correct
			while n:
				n -= 1
				acl.DeleteAce(n)
			sd.SetSecurityDescriptorDacl(0, acl, 0)

		if ACL.flags & SACL_SECURITY_INFORMATION:
			acl = sd.GetSecurityDescriptorSacl()
			if acl:
				n = acl.GetAceCount()
				# traverse the ACL in reverse, so the indices stay correct
				while n:
					n -= 1
					acl.DeleteAce(n)
				sd.SetSecurityDescriptorSacl(0, acl, 0)

		try:
			rp.conn.win32security. \
				SetNamedSecurityInfo(rp.path, SE_FILE_OBJECT, ACL.flags,
				sd.GetSecurityDescriptorOwner(),sd.GetSecurityDescriptorGroup(),
				sd.GetSecurityDescriptorDacl(),
				(ACL.flags & SACL_SECURITY_INFORMATION) and 
					sd.GetSecurityDescriptorSacl() or None)
		except (OSError, IOError, pywintypes.error), exc:
			log.Log("Warning: unable to set ACL on %s after clearing: %s"
					% (repr(rp.path), exc), 4)

	def write_to_rp(self, rp):
		if not self.__acl:
			return

		try:
			sd = rp.conn.win32security. \
				ConvertStringSecurityDescriptorToSecurityDescriptor(
					self.__acl, SDDL_REVISION_1)
		except (OSError, IOError, pywintypes.error), exc:
			log.Log("Warning: unable to convert string %s to ACL: %s"
				% (repr(self.__acl), exc), 4)

		# Enable next block of code for dirs after we have a mechanism in
		# backup.py (and similar) to do a first pass to see if a directory
		# has SE_DACL_PROTECTED. In that case, we will need to
		#		1) dest_rorp.write_win_acl(source_rorp.get_win_acl())
		#				--> And clear existing dest_rorp one while doing so
		#		2) Check if backup user has Admin privs to write dest_rorp
		#				--> May need to use Win32 AccessCheck() API
		#		3) If not, add Admin write privs to dest_rorp and add dir
		#				to dir_perms_list-equivalent
		#		4) THEN, allow the pre_process() function to finish and the
		#				files be copied over. Those files which wish to
		#				will now inherit the correct ACE objects.
		#		5) If dir was on dir_perms_list-equivalent, drop the write
		#				write permission we added.
		#		6) When copy_attribs is called in end_process, make sure
		#				that the write_win_acl() call isn't made this time
		# The reason we will need to do this is because otherwise, the files
		# which are created during step 4 will reference the ACE entries
		# which we clear during step 6. We need to clear them *before* the
		# children files/subdirs are created and generate the appropriate
		# DACL so the inheritance magic can happen during step 4.

		(flags, revision) = sd.GetSecurityDescriptorControl()
		if (not rp.isdir() and flags & SE_DACL_PROTECTED):
			self.clear_rp(rp)

		try:
			rp.conn.win32security. \
				SetNamedSecurityInfo(rp.path, SE_FILE_OBJECT, ACL.flags,
				sd.GetSecurityDescriptorOwner(),sd.GetSecurityDescriptorGroup(),
				sd.GetSecurityDescriptorDacl(),
				(ACL.flags & SACL_SECURITY_INFORMATION) and 
					sd.GetSecurityDescriptorSacl() or None)
		except (OSError, IOError, pywintypes.error), exc:
			log.Log("Warning: unable to set ACL on %s: %s"
					% (repr(rp.path), exc), 4)

	def __str__(self):
		return '# file: %s\n%s\n' % \
				(C.acl_quote(self.get_indexpath()), unicode(self.__acl))

	def from_string(self, acl_str):
		lines = acl_str.splitlines()
		if len(lines) != 2 or not lines[0][:8] == "# file: ":
			raise metadata.ParsingError("Bad record beginning: " + lines[0][:8])
		filename = lines[0][8:]
		if filename == '.': self.index = ()
		else: self.index = tuple(C.acl_unquote(filename).split('/'))
		self.__acl = lines[1]

def Record2WACL(record):
	acl = ACL()
	acl.from_string(record)
	return acl

def WACL2Record(wacl):
	return unicode(wacl)

class WACLExtractor(metadata.FlatExtractor):
	"""Iterate ExtendedAttributes objects from the WACL information file"""
	record_boundary_regexp = re.compile('(?:\\n|^)(# file: (.*?))\\n')
	record_to_object = staticmethod(Record2WACL)
	def filename_to_index(self, filename):
		"""Convert possibly quoted filename to index tuple"""
		if filename == '.': return ()
		else: return tuple(C.acl_unquote(filename).split('/'))

class WinAccessControlListFile(metadata.FlatFile):
	"""Store/retrieve ACLs from extended_attributes file"""
	_prefix = "win_access_control_lists"
	_extractor = WACLExtractor
	_object_to_record = staticmethod(WACL2Record)

def join_wacl_iter(rorp_iter, wacl_iter):
	"""Update a rorp iter by adding the information from acl_iter"""
	for rorp, wacl in rorpiter.CollateIterators(rorp_iter, wacl_iter):
		assert rorp, "Missing rorp for index %s" % (wacl.index,)
		if not wacl: wacl = ACL(rorp.index)
		rorp.set_win_acl(unicode(wacl))
		yield rorp
	
def rpath_acl_win_get(rpath):
	acl = ACL()
	acl.load_from_rp(rpath)
	return unicode(acl)
rpath.win_acl_get = rpath_acl_win_get

def rpath_get_blank_win_acl(index):
	acl = ACL(index)
	return unicode(acl)
rpath.get_blank_win_acl = rpath_get_blank_win_acl

def rpath_set_win_acl(rp, acl_str):
	acl = ACL()
	acl.from_string(acl_str)
	acl.write_to_rp(rp)
rpath.write_win_acl = rpath_set_win_acl

def init_acls():
	# A process that tries to read or write a SACL needs
	# to have and enable the SE_SECURITY_NAME privilege.
	# And inorder to backup/restore, the SE_BACKUP_NAME and
	# SE_RESTORE_NAME privileges are needed.
	import win32api
	try:
		hnd = OpenProcessToken(win32api.GetCurrentProcess(),
			TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)
	except win32api.error, exc:
		log.Log("Warning: unable to open Windows process token: %s"
				% exc, 5)
		return
	try:
		try:
			lpv = lambda priv: LookupPrivilegeValue(None, priv)
			# enable the SE_*_NAME privileges 
			SecurityName = lpv(SE_SECURITY_NAME)
			AdjustTokenPrivileges(hnd, False, [
				(SecurityName, SE_PRIVILEGE_ENABLED),
				(lpv(SE_BACKUP_NAME), SE_PRIVILEGE_ENABLED),
				(lpv(SE_RESTORE_NAME), SE_PRIVILEGE_ENABLED)
				])
		except win32api.error, exc:
			log.Log("Warning: unable to enable SE_*_NAME privileges: %s"
				% exc, 5)
			return
		for name, enabled in GetTokenInformation(hnd, TokenPrivileges):
			if name == SecurityName and enabled:
				# now we *may* access the SACL (sigh)
				ACL.flags |= SACL_SECURITY_INFORMATION
				break
	finally:
		win32api.CloseHandle(hnd)
