Redbrick User management tool
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
14 KiB

  1. #-----------------------------------------------------------------------------#
  2. # MODULE DESCRIPTION #
  3. #-----------------------------------------------------------------------------#
  4. """RedBrick Account Module; contains RBAccount class."""
  5. # System modules
  6. import grp
  7. import os
  8. import pwd
  9. import random
  10. import re
  11. import shutil
  12. import sys
  13. # RedBrick modules
  14. import rbconfig
  15. from rberror import *
  16. from rbopt import *
  17. from rbuser import *
  18. #-----------------------------------------------------------------------------#
  19. # DATA #
  20. #-----------------------------------------------------------------------------#
  21. __version__ = '$Revision: 1.8 $'
  22. __author__ = 'Cillian Sharkey'
  23. #-----------------------------------------------------------------------------#
  24. # CLASSES #
  25. #-----------------------------------------------------------------------------#
  26. class RBAccount:
  27. """Class to interface with Unix accounts."""
  28. def __init__(self):
  29. """Create new RBAccount object."""
  30. self.opt = RBOpt()
  31. def setopt(self, opt):
  32. """Use given RBOpt object to retrieve options."""
  33. self.opt = opt
  34. #---------------------------------------------------------------------#
  35. # SINGLE ACCOUNT METHODS #
  36. #---------------------------------------------------------------------#
  37. def add(self, usr):
  38. """Add account."""
  39. # Create home and webtree directory and populate.
  40. #
  41. webtree = rbconfig.gen_webtree(usr.uid)
  42. self.wrapper(os.mkdir, webtree, 0711)
  43. self.wrapper(os.chown, webtree, usr.uidNumber, usr.gidNumber)
  44. self.cmd('%s -Rp %s %s' % (rbconfig.command_cp, rbconfig.dir_skel, usr.homeDirectory))
  45. self.wrapper(os.chmod, usr.homeDirectory, 0711)
  46. self.wrapper(os.symlink, webtree, '%s/public_html' % usr.homeDirectory)
  47. #symlink vuln fix
  48. try:
  49. self.wrapper(os.chown, usr.homeDirectory+'/public_html', usr.uidNumber, usr.gidNumber)
  50. except OSError:
  51. pass
  52. # Add a .forward file in their home directory to point to their
  53. # alternate email address, but only if they're a dcu person and
  54. # have an alternate email that's not a redbrick address.
  55. #
  56. if usr.usertype in rbconfig.usertypes_dcu and usr.altmail and not re.search(r'@.*redbrick\.dcu\.ie', usr.altmail):
  57. forward_file = '%s/.forward' % usr.homeDirectory
  58. fd = self.my_open(forward_file)
  59. fd.write('%s\n' % usr.altmail)
  60. self.my_close(fd)
  61. self.wrapper(os.chmod, forward_file, 0600)
  62. # Change user & group ownership recursively on home directory.
  63. #
  64. self.cmd('%s -Rh %s:%s %s' % (rbconfig.command_chown, usr.uidNumber, usr.usertype, self.shquote(usr.homeDirectory)))
  65. # Set quotas for each filesystem.
  66. #
  67. for fs, (bqs, bqh, iqs, iqh) in rbconfig.gen_quotas(usr.usertype).items():
  68. self.quota_set(usr.uidNumber, fs, bqs, bqh, iqs, iqh)
  69. # Add to redbrick announcement mailing lists.
  70. #
  71. self.list_add('announce-redbrick', '%s@redbrick.dcu.ie' % usr.uid)
  72. self.list_add('redbrick-newsletter', '%s@redbrick.dcu.ie' % usr.uid)
  73. def delete(self, usr):
  74. """Delete a local Unix account."""
  75. # Zero out quotas.
  76. #
  77. for fs in rbconfig.gen_quotas().keys():
  78. self.quota_delete(usr.uidNumber, fs)
  79. # Remove home directory and webtree. Don't bomb out if the
  80. # directories don't exist (i.e. ignore OSError).
  81. #
  82. try:
  83. self.wrapper(shutil.rmtree, usr.homeDirectory)
  84. except OSError:
  85. pass
  86. try:
  87. self.wrapper(shutil.rmtree, rbconfig.gen_webtree(usr.uid))
  88. except OSError:
  89. pass
  90. # Remove from announce mailing lists.
  91. #
  92. self.list_delete('announce-redbrick', '%s@redbrick.dcu.ie' % usr.uid);
  93. self.list_delete('redbrick-newsletter', '%s@redbrick.dcu.ie' % usr.uid);
  94. for file in rbconfig.gen_extra_user_files(usr.uid):
  95. try:
  96. self.wrapper(os.unlink, file)
  97. except OSError:
  98. pass
  99. def rename(self, oldusr, newusr):
  100. """Rename an account.
  101. Requires: oldusr.uid, oldusr.homeDirectory, newusr.uid,
  102. newusr.homeDirectory.
  103. """
  104. # XXX Should check this before we rename user in ldap, have a
  105. # rbaccount.check_userfree? There should never be a file or
  106. # directory in /home or /webtree that doesn't belong to an
  107. # existing user.
  108. if os.path.exists(newusr.homeDirectory):
  109. if not os.path.isdir(newusr.homeDirectory):
  110. try:
  111. self.wrapper(os.unlink, newusr.homeDirectory)
  112. except OSError:
  113. raise RBFatalError("New home directory '%s' already exists, could not unlink existing file." % newusr.homeDirectory)
  114. else:
  115. raise RBFatalError("New home directory '%s' already exists." % newusr.homeDirectory)
  116. oldwebtree = rbconfig.gen_webtree(oldusr.uid)
  117. newwebtree = rbconfig.gen_webtree(newusr.uid)
  118. try:
  119. self.wrapper(os.rename, oldusr.homeDirectory, newusr.homeDirectory)
  120. except OSError, e:
  121. raise RBFatalError("Could not rename home directory [%s]" % e)
  122. try:
  123. self.wrapper(os.rename, oldwebtree, newwebtree)
  124. except OSError, e:
  125. raise RBFatalError("Could not rename webtree directory [%s]" % e)
  126. # Remove and then attempt to rename webtree symlink.
  127. #
  128. webtreelink = '%s/public_html' % newusr.homeDirectory
  129. try:
  130. self.wrapper(os.unlink, webtreelink)
  131. except OSError:
  132. pass
  133. if not os.path.exists(webtreelink):
  134. self.wrapper(os.symlink, newwebtree, webtreelink)
  135. # Rename any extra files that may belong to a user.
  136. #
  137. oldfiles = rbconfig.gen_extra_user_files(oldusr.uid)
  138. newfiles = rbconfig.gen_extra_user_files(newusr.uid)
  139. for i in range(len(oldfiles)):
  140. oldf = oldfiles[i]
  141. newf = newfiles[i]
  142. try:
  143. if os.path.isfile(oldf):
  144. self.wrapper(os.rename, oldf, newf)
  145. except OSError,e :
  146. raise RBFatalError("Could not rename '%s' to '%s' [%s]" % (oldf, newf, e))
  147. # XXX
  148. # Rename their subscription to announce lists in case an email
  149. # alias isn't put in for them or is later removed.
  150. #
  151. self.list_delete('announce-redbrick', "%s@redbrick.dcu.ie" % oldusr.uid);
  152. self.list_delete('redbrick-newsletter', "%s@redbrick.dcu.ie" % oldusr.uid);
  153. self.list_add('announce-redbrick', "%s@redbrick.dcu.ie" % newusr.uid);
  154. self.list_add('redbrick-newsletter', "%s@redbrick.dcu.ie" % newusr.uid);
  155. def convert(self, oldusr, newusr):
  156. """Convert account to a new usertype (Unix group)."""
  157. if oldusr.usertype == newusr.usertype:
  158. return
  159. # Do supplementary group shit in rbuserdb.
  160. #
  161. #if rbconfig.convert_primary_groups.has_key(usertype):
  162. # group = rbconfig.convert_primary_groups[usertype]
  163. #else:
  164. # group = usertype
  165. #if rbconfig.convert_extra_groups.has_key(usertype):
  166. # groups = '-G ' + rbconfig.convert_extra_groups[usertype]
  167. #else:
  168. # groups = ''
  169. if newusr.usertype == 'committe' and oldusr.usertype not in ('member', 'staff', 'committe'):
  170. raise RBFatalError("Non-members cannot be converted to committee group")
  171. if os.path.exists(newusr.homeDirectory):
  172. if not os.path.isdir(newusr.homeDirectory):
  173. try:
  174. self.wrapper(os.unlink, newusr.homeDirectory)
  175. except OSError:
  176. raise RBFatalError("New home directory '%s' already exists, could not unlink existing file." % newusr.homeDirectory)
  177. else:
  178. raise RBFatalError("New home directory '%s' already exists." % newusr.homeDirectory)
  179. # Rename home directory.
  180. #
  181. try:
  182. self.wrapper(os.rename, oldusr.homeDirectory, newusr.homeDirectory)
  183. except:
  184. raise RBFatalError("Could not rename home directory")
  185. # Change the home directory and webtree ownership to the new
  186. # group. -h on Solaris chgrp makes sure to change the symbolic
  187. # links themselves not the files they point to - very
  188. # important!!
  189. #
  190. self.cmd("%s -Rh %s %s %s" % (rbconfig.command_chgrp, newusr.gidNumber, self.shquote(newusr.homeDirectory), self.shquote(rbconfig.gen_webtree(oldusr.uid))))
  191. # Add/remove from committee mailing list as appropriate.
  192. #
  193. if newusr.usertype == 'committe':
  194. self.list_add('committee', "%s@redbrick.dcu.ie" % oldusr.uid)
  195. elif oldusr.usertype == 'committe':
  196. self.list_delete('committee', "%s@redbrick.dcu.ie" % oldusr.uid)
  197. # Add to admin list. Most admins stay in the root group for a while
  198. # after leaving committee, so removal can be done manually later.
  199. #
  200. if newusr.usertype == 'admin':
  201. self.list_add('rb-admins', "%s@redbrick.dcu.ie" % oldusr.uid)
  202. def disuser(self, username, disuser_period = None):
  203. """Disable an account with optional automatic re-enabling after
  204. given period."""
  205. #TODO
  206. def reuser(self, username):
  207. """Re-enable an account."""
  208. #TODO
  209. def quota_set(self, username, fs, bqs, bqh, iqs, iqh):
  210. """Set given quota for given username on given filesystem.
  211. Format for quota values is the same as that used for quotas
  212. function in rbconfig module."""
  213. self.cmd("%s -r %s %d %d %d %d %s" % (rbconfig.command_setquota, self.shquote(str(username)), bqs, bqh, iqs, iqh, fs))
  214. #self.cmd("%s -b %d -B %d -i %d -I %d %s %s" % (rbconfig.command_setquota, bqs, bqh, iqs, iqh, fs, self.shquote(str(username))))
  215. def quota_delete(self, username, fs):
  216. """Delete quota for given username on given filesystem."""
  217. self.quota_set(username, fs, 0, 0, 0, 0)
  218. #self.cmd('%s -d %s %s' % (rbconfig.command_setquota, fs, self.shquote(str(username))))
  219. #---------------------------------------------------------------------#
  220. # SINGLE ACCOUNT INFORMATION METHODS #
  221. #---------------------------------------------------------------------#
  222. def show(self, usr):
  223. """Show account details on standard output."""
  224. print "%13s:" % 'homedir mode',
  225. if os.path.isdir(usr.homeDirectory):
  226. print '%04o' % (os.stat(usr.homeDirectory)[0] & 07777)
  227. else:
  228. print 'Home directory does not exist'
  229. print "%13s: %s" % ('logged in', os.path.exists('%s/%s' % (rbconfig.dir_signaway_state, usr.uid)) and 'true' or 'false')
  230. #---------------------------------------------------------------------#
  231. # USER CHECKING AND INFORMATION RETRIEVAL METHODS #
  232. #---------------------------------------------------------------------#
  233. def check_accountfree(self, usr):
  234. """Raise RBFatalError if given account name is not free i.e.
  235. has a home directory."""
  236. if os.path.exists(usr.homeDirectory):
  237. raise RBFatalError("Account '%s' already exists (has a home directory)" % usr.uid)
  238. def check_account_byname(self, usr):
  239. """Raise RBFatalError if given account does not exist."""
  240. if not os.path.exists(usr.homeDirectory):
  241. raise RBFatalError("Account '%s' does not exist (no home directory)" % usr.uid)
  242. #---------------------------------------------------------------------#
  243. # OTHER METHODS #
  244. #---------------------------------------------------------------------#
  245. def list_add(self, list, email):
  246. """Add email address to mailing list."""
  247. fd = self.my_popen("su -c '%s/bin/add_members -r - %s' list" % (rbconfig.dir_mailman, self.shquote(list)))
  248. fd.write('%s\n' % email)
  249. self.my_close(fd)
  250. def list_delete(self, list, email):
  251. """Delete email address from a mailing list."""
  252. self.runcmd("su -c '%s/bin/remove_members %s %s' list" % (rbconfig.dir_mailman, self.shquote(list), self.shquote(email)))
  253. #--------------------------------------------------------------------#
  254. # INTERNAL METHODS #
  255. #--------------------------------------------------------------------#
  256. def shquote(self, string):
  257. """Return a quoted string suitable to use with shell safely."""
  258. return "'" + string.replace("'", r"'\''") + "'"
  259. def runcmd(self, cmd):
  260. """runcmd(command) -> output, status
  261. Run given command and return command output (stdout & stderr combined)
  262. and exit status.
  263. """
  264. if self.opt.test:
  265. print >> sys.stderr, "TEST: runcmd:", cmd
  266. return None, None
  267. else:
  268. fd = os.popen(cmd + ' 2>&1')
  269. return fd.read(), fd.close()
  270. def cmd(self, cmd):
  271. """Run given command and raise a RBError exception returning
  272. the command output if command exit status is non zero."""
  273. output, status = self.runcmd(cmd)
  274. if status:
  275. raise RBFatalError("Command '%s' failed.\n%s" % (cmd, output))
  276. def wrapper(self, function, *keywords, **arguments):
  277. """Wrapper method for executing other functions.
  278. If test mode is set, print function name and arguments.
  279. Otherwise call function with arguments.
  280. """
  281. if self.opt.test:
  282. sys.stderr.write("TEST: %s(" % function.__name__)
  283. for i in keywords:
  284. sys.stderr.write("%s, " % (i,))
  285. for k, v in arguments.items():
  286. sys.stderr.write("%s = %s, " % (k, v))
  287. sys.stderr.write(")\n")
  288. else:
  289. return function(*keywords, **arguments)
  290. def my_open(self, file):
  291. """Return file descriptor to given file for writing."""
  292. if self.opt.test:
  293. print >> sys.stderr, 'TEST: open:', file
  294. return sys.stderr
  295. else:
  296. return open(file, 'w')
  297. def my_popen(self, cmd):
  298. """Return file descriptor to given command pipe for writing."""
  299. if self.opt.test:
  300. print >> sys.stderr, 'TEST: popen:', cmd
  301. return sys.stderr
  302. else:
  303. return os.popen(cmd, 'w')
  304. def my_close(self, fd):
  305. """Close given pipe returned by _[p]open."""
  306. if not self.opt.test:
  307. fd.close()
  308. #--------------------------------------------------------------------#
  309. # ERROR HANDLING #
  310. #--------------------------------------------------------------------#
  311. def rberror(self, e):
  312. """Handle errors."""
  313. if self.opt.override and isinstance(e, RBWarningError):
  314. return
  315. # If we reach here it's either a FATAL error or there was no
  316. # override for a WARNING error, so raise it again to let the
  317. # caller handle it.
  318. #
  319. raise e