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