command.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. """module to handle command files
  2. @contact: Debian FTP Master <ftpmaster@debian.org>
  3. @copyright: 2012, Ansgar Burchardt <ansgar@debian.org>
  4. @license: GPL-2+
  5. """
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License along
  17. # with this program; if not, write to the Free Software Foundation, Inc.,
  18. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  19. import apt_pkg
  20. import os
  21. import tempfile
  22. from daklib.config import Config
  23. from daklib.dbconn import *
  24. from daklib.gpg import SignedFile
  25. from daklib.regexes import re_field_package
  26. from daklib.textutils import fix_maintainer
  27. from daklib.utils import gpg_get_key_addresses, send_mail, TemplateSubst
  28. class CommandError(Exception):
  29. pass
  30. class CommandFile(object):
  31. def __init__(self, filename, data, log=None):
  32. if log is None:
  33. from daklib.daklog import Logger
  34. log = Logger()
  35. self.cc = []
  36. self.result = []
  37. self.log = log
  38. self.filename = filename
  39. self.data = data
  40. def _check_replay(self, signed_file, session):
  41. """check for replays
  42. @note: Will commit changes to the database.
  43. @type signed_file: L{daklib.gpg.SignedFile}
  44. @param session: database session
  45. """
  46. # Mark commands file as seen to prevent replays.
  47. signature_history = SignatureHistory.from_signed_file(signed_file)
  48. session.add(signature_history)
  49. session.commit()
  50. def _quote_section(self, section):
  51. lines = []
  52. for l in str(section).splitlines():
  53. lines.append("> {0}".format(l))
  54. return "\n".join(lines)
  55. def _evaluate_sections(self, sections, session):
  56. session.rollback()
  57. try:
  58. while True:
  59. sections.next()
  60. section = sections.section
  61. self.result.append(self._quote_section(section))
  62. action = section.get('Action', None)
  63. if action is None:
  64. raise CommandError('Encountered section without Action field')
  65. commandactions = {
  66. 'dm': self.action_dm,
  67. 'dm-remove': self.action_dm_remove,
  68. 'dm-migrate': self.action_dm_migrate,
  69. 'break-the-archive': self.action_break_the_archive,
  70. 'bikeshed-create': self.action_create_bs, # create a bs
  71. 'bikeshed-drop': self.action_drop_bs, # drop a bs
  72. 'bikeshed-demolish': self.action_drop_bs, # drop a bs
  73. 'bikeshed-modify': self.action_modify_bs, # modify bs (breaks/depends)
  74. 'bikeshed-paint': self.action_modify_bs, # modify bs (breaks/depends)
  75. 'bikeshed-acl': self.action_acls_bs, # modify access list
  76. 'bikeshed-add-pkg': self.action_add_pkg_bs, # add a package from another suite
  77. 'bikeshed-rm-pkg': self.action_rm_pkg_bs, # remove a package from a bs
  78. 'bikeshed-mergeback': self.action_mergeback_bs, # merge contents back in base suite
  79. 'bikeshed-list': self.action_list_bs, # list bs for fingerprint (where there is any access)
  80. 'bikeshed-info': self.action_info_bs, # info about a specific bs
  81. }
  82. func = commandactions.get(action, self.action_unknown_command)
  83. func(self.fingerprint, section, session, action)
  84. self.result.append('')
  85. except StopIteration:
  86. pass
  87. finally:
  88. session.rollback()
  89. def _notify_uploader(self):
  90. cnf = Config()
  91. bcc = 'X-DAK: dak process-command'
  92. if 'Dinstall::Bcc' in cnf:
  93. bcc = '{0}\nBcc: {1}'.format(bcc, cnf['Dinstall::Bcc'])
  94. cc = set(fix_maintainer(address)[1] for address in self.cc)
  95. subst = {
  96. '__DAK_ADDRESS__': cnf['Dinstall::MyEmailAddress'],
  97. '__MAINTAINER_TO__': fix_maintainer(self.uploader)[1],
  98. '__CC__': ", ".join(cc),
  99. '__BCC__': bcc,
  100. '__RESULTS__': "\n".join(self.result),
  101. '__FILENAME__': self.filename,
  102. }
  103. message = TemplateSubst(subst, os.path.join(cnf['Dir::Templates'], 'process-command.processed'))
  104. send_mail(message)
  105. def evaluate(self):
  106. """evaluate commands file
  107. @rtype: bool
  108. @returns: C{True} if the file was processed sucessfully,
  109. C{False} otherwise
  110. """
  111. result = True
  112. session = DBConn().session()
  113. keyrings = session.query(Keyring).filter_by(active=True).order_by(Keyring.priority)
  114. keyring_files = [k.keyring_name for k in keyrings]
  115. signed_file = SignedFile(self.data, keyring_files)
  116. if not signed_file.valid:
  117. self.log.log(['invalid signature', self.filename])
  118. return False
  119. self.fingerprint = session.query(Fingerprint).filter_by(fingerprint=signed_file.primary_fingerprint).one()
  120. if self.fingerprint.keyring is None:
  121. self.log.log(['singed by key in unknown keyring', self.filename])
  122. return False
  123. assert self.fingerprint.keyring.active
  124. self.log.log(['processing', self.filename, 'signed-by={0}'.format(self.fingerprint.fingerprint)])
  125. with tempfile.TemporaryFile() as fh:
  126. fh.write(signed_file.contents)
  127. fh.seek(0)
  128. sections = apt_pkg.TagFile(fh)
  129. self.uploader = None
  130. addresses = gpg_get_key_addresses(self.fingerprint.fingerprint)
  131. if len(addresses) > 0:
  132. self.uploader = addresses[0]
  133. try:
  134. sections.next()
  135. section = sections.section
  136. if 'Uploader' in section:
  137. self.uploader = section['Uploader']
  138. if 'Cc' in section:
  139. self.cc.append(section['Cc'])
  140. # TODO: Verify first section has valid Archive field
  141. if 'Archive' not in section:
  142. raise CommandError('No Archive field in first section.')
  143. # TODO: send mail when we detected a replay.
  144. self._check_replay(signed_file, session)
  145. self._evaluate_sections(sections, session)
  146. self.result.append('')
  147. except Exception as e:
  148. self.log.log(['ERROR', e])
  149. self.result.append("There was an error processing this section. No changes were committed.\nDetails:\n{0}".format(e))
  150. result = False
  151. self._notify_uploader()
  152. session.close()
  153. return result
  154. def _split_packages(self, value):
  155. names = value.split()
  156. for name in names:
  157. if not re_field_package.match(name):
  158. raise CommandError('Invalid package name "{0}"'.format(name))
  159. return names
  160. def action_unknown_command(self, fingerprint, section, session, action):
  161. raise CommandError('Unknown action: {0}'.format(action))
  162. def action_dm(self, fingerprint, section, session, action=None):
  163. cnf = Config()
  164. if 'Command::DM::AdminKeyrings' not in cnf \
  165. or 'Command::DM::ACL' not in cnf \
  166. or 'Command::DM::Keyrings' not in cnf:
  167. raise CommandError('DM command is not configured for this archive.')
  168. allowed_keyrings = cnf.value_list('Command::DM::AdminKeyrings')
  169. if fingerprint.keyring.keyring_name not in allowed_keyrings:
  170. raise CommandError('Key {0} is not allowed to set DM'.format(fingerprint.fingerprint))
  171. acl_name = cnf.get('Command::DM::ACL', 'dm')
  172. acl = session.query(ACL).filter_by(name=acl_name).one()
  173. fpr_hash = section['Fingerprint'].translate(None, ' ')
  174. fpr = session.query(Fingerprint).filter_by(fingerprint=fpr_hash).first()
  175. if fpr is None:
  176. raise CommandError('Unknown fingerprint {0}'.format(fpr_hash))
  177. if fpr.keyring is None or fpr.keyring.keyring_name not in cnf.value_list('Command::DM::Keyrings'):
  178. raise CommandError('Key {0} is not in DM keyring.'.format(fpr.fingerprint))
  179. addresses = gpg_get_key_addresses(fpr.fingerprint)
  180. if len(addresses) > 0:
  181. self.cc.append(addresses[0])
  182. self.log.log(['dm', 'fingerprint', fpr.fingerprint])
  183. self.result.append('Fingerprint: {0}'.format(fpr.fingerprint))
  184. if len(addresses) > 0:
  185. self.log.log(['dm', 'uid', addresses[0]])
  186. self.result.append('Uid: {0}'.format(addresses[0]))
  187. for source in self._split_packages(section.get('Allow', '')):
  188. # Check for existance of source package to catch typos
  189. if session.query(DBSource).filter_by(source=source).first() is None:
  190. raise CommandError('Tried to grant permissions for unknown source package: {0}'.format(source))
  191. if session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr, source=source).first() is None:
  192. aps = ACLPerSource()
  193. aps.acl = acl
  194. aps.fingerprint = fpr
  195. aps.source = source
  196. aps.created_by = fingerprint
  197. aps.reason = section.get('Reason')
  198. session.add(aps)
  199. self.log.log(['dm', 'allow', fpr.fingerprint, source])
  200. self.result.append('Allowed: {0}'.format(source))
  201. else:
  202. self.result.append('Already-Allowed: {0}'.format(source))
  203. session.flush()
  204. for source in self._split_packages(section.get('Deny', '')):
  205. count = session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr, source=source).delete()
  206. if count == 0:
  207. raise CommandError('Tried to remove upload permissions for package {0}, '
  208. 'but no upload permissions were granted before.'.format(source))
  209. self.log.log(['dm', 'deny', fpr.fingerprint, source])
  210. self.result.append('Denied: {0}'.format(source))
  211. session.commit()
  212. def _action_dm_admin_common(self, fingerprint, section, session):
  213. cnf = Config()
  214. if 'Command::DM-Admin::AdminFingerprints' not in cnf \
  215. or 'Command::DM::ACL' not in cnf:
  216. raise CommandError('DM admin command is not configured for this archive.')
  217. allowed_fingerprints = cnf.value_list('Command::DM-Admin::AdminFingerprints')
  218. if fingerprint.fingerprint not in allowed_fingerprints:
  219. raise CommandError('Key {0} is not allowed to admin DM'.format(fingerprint.fingerprint))
  220. def action_dm_remove(self, fingerprint, section, session, action=None):
  221. self._action_dm_admin_common(fingerprint, section, session)
  222. cnf = Config()
  223. acl_name = cnf.get('Command::DM::ACL', 'dm')
  224. acl = session.query(ACL).filter_by(name=acl_name).one()
  225. fpr_hash = section['Fingerprint'].translate(None, ' ')
  226. fpr = session.query(Fingerprint).filter_by(fingerprint=fpr_hash).first()
  227. if fpr is None:
  228. self.result.append('Unknown fingerprint: {0}\nNo action taken.'.format(fpr_hash))
  229. return
  230. self.log.log(['dm-remove', fpr.fingerprint])
  231. count = 0
  232. for entry in session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr):
  233. self.log.log(['dm-remove', fpr.fingerprint, 'source={0}'.format(entry.source)])
  234. count += 1
  235. session.delete(entry)
  236. self.result.append('Removed: {0}.\n{1} acl entries removed.'.format(fpr.fingerprint, count))
  237. session.commit()
  238. def action_dm_migrate(self, fingerprint, section, session, action=None):
  239. self._action_dm_admin_common(fingerprint, section, session)
  240. cnf = Config()
  241. acl_name = cnf.get('Command::DM::ACL', 'dm')
  242. acl = session.query(ACL).filter_by(name=acl_name).one()
  243. fpr_hash_from = section['From'].translate(None, ' ')
  244. fpr_from = session.query(Fingerprint).filter_by(fingerprint=fpr_hash_from).first()
  245. if fpr_from is None:
  246. self.result.append('Unknown fingerprint (From): {0}\nNo action taken.'.format(fpr_hash_from))
  247. return
  248. fpr_hash_to = section['To'].translate(None, ' ')
  249. fpr_to = session.query(Fingerprint).filter_by(fingerprint=fpr_hash_to).first()
  250. if fpr_to is None:
  251. self.result.append('Unknown fingerprint (To): {0}\nNo action taken.'.format(fpr_hash_to))
  252. return
  253. if fpr_to.keyring is None or fpr_to.keyring.keyring_name not in cnf.value_list('Command::DM::Keyrings'):
  254. self.result.append('Key (To) {0} is not in DM keyring.\nNo action taken.'.format(fpr_to.fingerprint))
  255. return
  256. self.log.log(['dm-migrate', 'from={0}'.format(fpr_hash_from), 'to={0}'.format(fpr_hash_to)])
  257. sources = []
  258. for entry in session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr_from):
  259. self.log.log(['dm-migrate', 'from={0}'.format(fpr_hash_from), 'to={0}'.format(fpr_hash_to), 'source={0}'.format(entry.source)])
  260. entry.fingerprint = fpr_to
  261. sources.append(entry.source)
  262. self.result.append('Migrated {0} to {1}.\n{2} acl entries changed: {3}'.format(fpr_hash_from, fpr_hash_to, len(sources), ", ".join(sources)))
  263. session.commit()
  264. def action_break_the_archive(self, fingerprint, section, session, action=None):
  265. name = 'Dave'
  266. uid = fingerprint.uid
  267. if uid is not None and uid.name is not None:
  268. name = uid.name.split()[0]
  269. self.result.append("DAK9000: I'm sorry, {0}. I'm afraid I can't do that.".format(name))
  270. def action_create_bs(self, fingerprint, section, session, action=None):
  271. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  272. def action_drop_bs(self, fingerprint, section, session, action=None):
  273. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  274. def action_modify_bs(self, fingerprint, section, session, action=None):
  275. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  276. def action_acl_bs(self, fingerprint, section, session, action=None):
  277. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  278. def action_add_pkg_bs(self, fingerprint, section, session, action=None):
  279. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  280. def action_rm_pkg_bs(self, fingerprint, section, session, action=None):
  281. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  282. def action_mergeback_bs(self, fingerprint, section, session, action=None):
  283. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  284. def action_list_bs(self, fingerprint, section, session, action=None):
  285. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))
  286. def action_info_bs(self, fingerprint, section, session, action=None):
  287. raise CommandError('Command {0} not implemented, what are you doing here?'.format(action))