#!/usr/bin/python
#
# Copyright 2015 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.

import os
import sys
from optparse import OptionParser
import traceback
import datetime

import univention.admin
import univention.admin.uldap
import univention.admin.config
import univention.admin.modules
import univention.admin.allocators as ualloc
from univention.config_registry import ConfigRegistry, handler_set, handler_unset

grp_blacklist = ["root", "daemon", "bin", "sys", "adm", "tty", "disk", "lp", "mail", "news", "uucp", "man", "proxy",
				 "kmem", "dialout", "fax", "voice", "cdrom", "floppy", "tape", "sudo", "audio", "dip", "www-data",
				 "backup", "operator", "list", "irc", "src", "gnats", "shadow", "utmp", "video", "sasl", "plugdev",
				 "staff", "games", "users", "nogroup", "libuuid", "crontab", "tss", "scanner", "nvram", "rdma", "fuse",
				 "kvm", "ssh", "apt-mirror", "ssl-cert", "postfix", "postdrop", "openldap", "nagios", "ntp", "bind",
				 "utempter", "sambashare", "winbindd_priv", "Domain Admins", "Domain Guests",
				 "Windows Hosts", "DC Backup Hosts", "DC Slave", "Hosts", "Computers", "Printer-Admins",
				 "Backup Join", "Slave Join", "Authenticated Users", "World Authority", "Everyone",
				 "Null Authority", "Nobody", "Enterprise Domain Controllers", "Remote Interactive Logon",
				 "SChannel Authentication", "Digest Authentication", "Terminal Server User",
				 "NTLM Authentication", "Other Organization", "This Organization", "Anonymous Logon", "Network Service",
				 "Creator Group", "Creator Owner", "Local Service", "Owner Rights",
				 "Interactive", "Restricted", "Network", "Service", "Dialup", "System", "Batch", "Proxy", "IUSR",
				 "Self", "Performance Log Users", "DnsUpdateProxy", "Cryptographic Operators", "Schema Admins",
				 "Backup Operators", "Administrators", "Domain Computers", "Windows Authorization Access Group",
				 "IIS_IUSRS", "RAS and IAS Servers", "Network Configuration Operators", "Account Operators",
				 "Distributed COM Users", "Read-Only Domain Controllers", "Terminal Server License Servers",
				 "Replicator", "Allowed RODC Password Replication Group", "Denied RODC Password Replication Group",
				 "Guests", "Users", "Enterprise Admins", "Group Policy Creator Owners", "Server Operators",
				 "Domain Controllers", "DnsAdmins", "Cert Publishers", "Incoming Forest Trust Builders",
				 "Event Log Readers", "Pre-Windows 2000 Compatible Access", "Remote Desktop Users",
				 "Performance Monitor Users", "Certificate Service DCOM Access",
				 "Enterprise Read-Only Domain Controllers"]

user_blacklist = ["Administrator", "krbtgt"]


univention.admin.modules.update()
univention.admin.syntax.update_choices()
lo, position = univention.admin.uldap.getAdminConnection()
co = univention.admin.config.config()
group_module = univention.admin.modules.get('groups/group')
user_module = univention.admin.modules.get('users/user')


def collect_gids():
	users = dict()
	for dn, attrs in lo.search('(&(uidNumber=*)(objectClass=posixAccount))', attr=('uidNumber', )):
		xid = attrs.get('uidNumber')
		if xid:
			users[(int(xid[0]))] = dn
	groups = dict()
	for dn, attrs in lo.search('(&(gidNumber=*)(objectClass=posixGroup))', attr=('gidNumber', )):
		xid = attrs.get('gidNumber')
		if xid:
			groups[(int(xid[0]))] = dn

	common_ids = set(users.keys()).intersection(set(groups.keys()))

	if options.verbose:
		print "# uidNumbers: %r" % users.keys()
		print "# gidNumbers: %r" % groups.keys()
		print "# common ids: %r" % common_ids

	res = list()
	for cid in common_ids:
		new_gidNumber = int(cid)
		while new_gidNumber in groups or new_gidNumber in users:
			new_gidNumber = int(ualloc.request(lo, position, 'gidNumber'))
		res.append((cid, new_gidNumber))
	return res

def write_gids(gids, filename):
	with open(filename, "w") as f:
		f.writelines(["%d %d\n" % gid for gid in gids])

def read_gids(filename):
	res = list()
	with open(filename, "r") as f:
		for line in f:
			try:
				from_gid, to_gid = line.strip().split()
				res.append((int(from_gid), int(to_gid)))
				if options.verbose:
					print "# %r -> %r" % (from_gid, to_gid)
			except IndexError:
				pass
			except ValueError:
				pass
	return res

def change_users_primary_groups(from_gid, to_gid, from_group):
	users = univention.admin.modules.lookup(user_module, co, lo, scope='sub', superordinate=None, filter="(gidNumber=%d)" % from_gid)
	bl = 0
	for user in users:
		if user["username"] in user_blacklist:
			err_file.write("USERPRIMGRPSET NOT changing blicklisted user %r from group %r to %r\n" % (user["username"], from_gid, to_gid))
			bl += 1
		else:
			err_file.write("USERPRIMGRPSET %r : %r -> %r\n" % (user["username"], from_gid, to_gid))
			user.open()
			user["primaryGroup"] = from_group.dn
			user.modify()
	return len(users) - bl

def change_gids(gids):
	for from_gid, to_gid in gids:
		try:
			from_group = univention.admin.modules.lookup(group_module, co, lo, scope='sub', superordinate=None, filter='(gidNumber=%d)' % from_gid)
			if len(from_group) == 1:
				from_group = from_group[0]
				if options.verbose:
					print "# %r -> %r (%r)" % (from_gid, to_gid, from_group['name'])
				if from_group["name"] in grp_blacklist:
					print "  not changing blacklisted group %s." % from_group["name"]
					continue
				from_group.open()
				from_group.descriptions["gidNumber"].may_change = 1
				from_group['gidNumber'] = str(to_gid)
				from_group.descriptions["gidNumber"].may_change = 0
				from_group.modify()
				users_changed = change_users_primary_groups(from_gid, to_gid, from_group)
				print "  %r users primary group changed." % users_changed
		except:
			print "Error changing %r -> %r, writing traceback to %s." % (from_gid, to_gid, err_file_name)
			err_file.write("GRPSET %r -> %r\n" % (from_gid, to_gid))
			err_file.write(traceback.format_exc())
			err_file.write("\n")

def walk_error(oserror):
	print "Error reading directory, writing traceback to %s." % err_file_name
	err_file.write("LIST %r\n" % oserror)

def chgrp(filepath, gid):
	uid = os.stat(filepath).st_uid
	os.chown(filepath, uid, gid)

def get_users_in_blacklisted_groups():
	users = dict()
	for groupname in grp_blacklist:
		group = univention.admin.modules.lookup(group_module, co, lo, scope='sub', superordinate=None, filter='(name=%s)' % groupname)
		if len(group) == 1:
			_users = univention.admin.modules.lookup(user_module, co, lo, scope='sub', superordinate=None, filter="(gidNumber=%s)" % group[0]["gidNumber"])
			for u in _users:
				try:
					users["%s (%s)" % (u["username"], u["uidNumber"])] += ", %s (%s)" %(groupname, group[0]["gidNumber"])
				except KeyError:
					users["%s (%s)" % (u["username"], u["uidNumber"])] = "%s (%s)" %(groupname, group[0]["gidNumber"])
	return users

parser = OptionParser()
parser.add_option("-b", "--blacklists", action="store_true", help="print blacklisted users and groups and exit [default=off].")
parser.add_option("-d", "--dir", dest="chgrp_directory",
				  help="run chgrp recursivly on directory, expects change information from file with -i",
				  metavar="FILE")
parser.add_option("-i", "--infile", dest="in_filename", help="read group changes from FILE", metavar="FILE")
parser.add_option("-o", "--outfile", dest="out_filename", help="write group changes to FILE", metavar="FILE")
parser.add_option("-l", "--ldap", action="store_true", help="Modify GIDs of groups in LDAP, expects change information from file with -i [default=off].")
parser.add_option("-v", "--verbose", action="store_false", dest="verbose", default=True,
                  help="write about current action [default=on].")

(options, args) = parser.parse_args()

if options.blacklists:
	print "User blacklist: %r\n" % user_blacklist
	print "Group blacklist: %r\n" % grp_blacklist
	print "Users with a blacklisted primary group: %r" % get_users_in_blacklisted_groups()
	sys.exit(0)
if options.chgrp_directory and not options.in_filename:
	parser.error("Please supply the directory that run chgrp should be run on recursivly with '-i'.")
if options.in_filename and options.out_filename:
	parser.error("Options -i and -o are mutually exclusive.")
if not options.in_filename and not options.out_filename and not options.blacklists:
	parser.error("Please use either -b, -i or -o.")
if options.in_filename and not (options.chgrp_directory or options.ldap):
	parser.error("Please use -i with -d or -l.")
if options.ldap and not options.in_filename:
	parser.error("Please supply data to change the group information in LDAP with '-i'.")
if options.ldap and options.out_filename:
	parser.error("Options -l and -o are mutually exclusive.")

try:
	os.mkdir("/var/log/changegrp/")
except OSError:
	pass
err_file_name = "/var/log/changegrp/%s.log" % datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
err_file = open(err_file_name, "ab")

if options.out_filename:
	gids = collect_gids()
	write_gids(gids, options.out_filename)
	print "Wrote %d GID pairs to %s." % (len(gids), options.out_filename)
	err_file.close()
	sys.exit(0)

if options.in_filename:
	print "Reading list of groups from %r..." % options.in_filename

	gids = read_gids(options.in_filename)
	print "... read %d GID pairs." % len(gids)

	if options.ldap:
		print "Changing %d gidNumbers in LDAP..." % len(gids)
		change_gids(gids)
		print "... done."
		err_file.close()
		sys.exit(0)

	if options.chgrp_directory:
		gids_dict = dict(gids)
		stats = {"dir": 0, "file": 0}
		print "Running chgrp recursivly on %r..." % options.chgrp_directory
		for root, dirs, files in os.walk(options.chgrp_directory, onerror=walk_error):
			print "#  %r..." % root
			for _dir in dirs:
				dir_gid = os.stat(os.path.join(root, _dir)).st_gid
				if dir_gid in gids_dict:
					stats["dir"] += 1
					chgrp(os.path.join(root, _dir), gids_dict[dir_gid])
					if options.verbose:
						err_file.write("CHGRP %r: %r -> %r\n" % (os.path.join(root, _dir), dir_gid, gids_dict[dir_gid]))
			for _file in files:
				file_gid = os.stat(os.path.join(root, _file)).st_gid
				if file_gid in gids_dict:
					stats["file"] += 1
					chgrp(os.path.join(root, _file), gids_dict[file_gid])
					if options.verbose:
						err_file.write("CHGRP %r: %r -> %r\n" % (os.path.join(root, _file), file_gid, gids_dict[file_gid]))
		print "... done. Changed permissions on %d directories and %d files." % (stats["dir"], stats["file"])
		err_file.close()
		sys.exit(0)
