1 """module to handle command files
3 @contact: Debian FTP Master <ftpmaster@debian.org>
4 @copyright: 2012, Ansgar Burchardt <ansgar@debian.org>
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.
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.
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.
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
34 class CommandError(Exception):
37 class CommandFile(object):
38 def __init__(self, filename, data, log=None):
40 from daklib.daklog import Logger
45 self.filename = filename
48 def _check_replay(self, signed_file, session):
51 @note: Will commit changes to the database.
53 @type signed_file: L{daklib.gpg.SignedFile}
55 @param session: database session
57 # Mark commands file as seen to prevent replays.
58 signature_history = SignatureHistory.from_signed_file(signed_file)
59 session.add(signature_history)
62 def _quote_section(self, section):
64 for l in str(section).splitlines():
65 lines.append("> {0}".format(l))
66 return "\n".join(lines)
68 def _evaluate_sections(self, sections, session):
73 section = sections.section
74 self.result.append(self._quote_section(section))
76 action = section.get('Action', None)
78 raise CommandError('Encountered section without Action field')
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)
89 raise CommandError('Unknown action: {0}'.format(action))
91 self.result.append('')
97 def _notify_uploader(self):
100 bcc = 'X-DAK: dak process-command'
101 if 'Dinstall::Bcc' in cnf:
102 bcc = '{0}\nBcc: {1}'.format(bcc, cnf['Dinstall::Bcc'])
104 cc = set(fix_maintainer(address)[1] for address in self.cc)
107 '__DAK_ADDRESS__': cnf['Dinstall::MyEmailAddress'],
108 '__MAINTAINER_TO__': fix_maintainer(self.uploader)[1],
109 '__CC__': ", ".join(cc),
111 '__RESULTS__': "\n".join(self.result),
112 '__FILENAME__': self.filename,
115 message = TemplateSubst(subst, os.path.join(cnf['Dir::Templates'], 'process-command.processed'))
120 """evaluate commands file
123 @returns: C{True} if the file was processed sucessfully,
128 session = DBConn().session()
130 keyrings = session.query(Keyring).filter_by(active=True).order_by(Keyring.priority)
131 keyring_files = [ k.keyring_name for k in keyrings ]
133 signed_file = SignedFile(self.data, keyring_files)
134 if not signed_file.valid:
135 self.log.log(['invalid signature', self.filename])
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])
142 assert self.fingerprint.keyring.active
144 self.log.log(['processing', self.filename, 'signed-by={0}'.format(self.fingerprint.fingerprint)])
146 with tempfile.TemporaryFile() as fh:
147 fh.write(signed_file.contents)
149 sections = apt_pkg.TagFile(fh)
152 addresses = gpg_get_key_addresses(self.fingerprint.fingerprint)
153 if len(addresses) > 0:
154 self.uploader = addresses[0]
158 section = sections.section
159 if 'Uploader' in section:
160 self.uploader = section['Uploader']
161 # TODO: Verify first section has valid Archive field
162 if 'Archive' not in section:
163 raise CommandError('No Archive field in first section.')
165 # TODO: send mail when we detected a replay.
166 self._check_replay(signed_file, session)
168 self._evaluate_sections(sections, session)
169 self.result.append('')
170 except Exception as e:
171 self.log.log(['ERROR', e])
172 self.result.append("There was an error processing this section. No changes were committed.\nDetails:\n{0}".format(e))
175 self._notify_uploader()
181 def _split_packages(self, value):
182 names = value.split()
184 if not re_field_package.match(name):
185 raise CommandError('Invalid package name "{0}"'.format(name))
188 def action_dm(self, fingerprint, section, session):
191 if 'Command::DM::AdminKeyrings' not in cnf \
192 or 'Command::DM::ACL' not in cnf \
193 or 'Command::DM::Keyrings' not in cnf:
194 raise CommandError('DM command is not configured for this archive.')
196 allowed_keyrings = cnf.value_list('Command::DM::AdminKeyrings')
197 if fingerprint.keyring.keyring_name not in allowed_keyrings:
198 raise CommandError('Key {0} is not allowed to set DM'.format(fingerprint.fingerprint))
200 acl_name = cnf.get('Command::DM::ACL', 'dm')
201 acl = session.query(ACL).filter_by(name=acl_name).one()
203 fpr_hash = section['Fingerprint'].translate(None, ' ')
204 fpr = session.query(Fingerprint).filter_by(fingerprint=fpr_hash).first()
206 raise CommandError('Unknown fingerprint {0}'.format(fpr_hash))
207 if fpr.keyring is None or fpr.keyring.keyring_name not in cnf.value_list('Command::DM::Keyrings'):
208 raise CommandError('Key {0} is not in DM keyring.'.format(fpr.fingerprint))
209 addresses = gpg_get_key_addresses(fpr.fingerprint)
210 if len(addresses) > 0:
211 self.cc.append(addresses[0])
213 self.log.log(['dm', 'fingerprint', fpr.fingerprint])
214 self.result.append('Fingerprint: {0}'.format(fpr.fingerprint))
215 if len(addresses) > 0:
216 self.log.log(['dm', 'uid', addresses[0]])
217 self.result.append('Uid: {0}'.format(addresses[0]))
219 for source in self._split_packages(section.get('Allow', '')):
220 # Check for existance of source package to catch typos
221 if session.query(DBSource).filter_by(source=source).first() is None:
222 raise CommandError('Tried to grant permissions for unknown source package: {0}'.format(source))
224 if session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr, source=source).first() is None:
227 aps.fingerprint = fpr
229 aps.created_by = fingerprint
230 aps.reason = section.get('Reason')
232 self.log.log(['dm', 'allow', fpr.fingerprint, source])
233 self.result.append('Allowed: {0}'.format(source))
235 self.result.append('Already-Allowed: {0}'.format(source))
239 for source in self._split_packages(section.get('Deny', '')):
240 count = session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr, source=source).delete()
242 raise CommandError('Tried to remove upload permissions for package {0}, '
243 'but no upload permissions were granted before.'.format(source))
245 self.log.log(['dm', 'deny', fpr.fingerprint, source])
246 self.result.append('Denied: {0}'.format(source))
250 def _action_dm_admin_common(self, fingerprint, section, session):
253 if 'Command::DM-Admin::AdminFingerprints' not in cnf \
254 or 'Command::DM::ACL' not in cnf:
255 raise CommandError('DM admin command is not configured for this archive.')
257 allowed_fingerprints = cnf.value_list('Command::DM-Admin::AdminFingerprints')
258 if fingerprint.fingerprint not in allowed_fingerprints:
259 raise CommandError('Key {0} is not allowed to admin DM'.format(fingerprint.fingerprint))
261 def action_dm_remove(self, fingerprint, section, session):
262 self._action_dm_admin_common(fingerprint, section, session)
265 acl_name = cnf.get('Command::DM::ACL', 'dm')
266 acl = session.query(ACL).filter_by(name=acl_name).one()
268 fpr_hash = section['Fingerprint'].translate(None, ' ')
269 fpr = session.query(Fingerprint).filter_by(fingerprint=fpr_hash).first()
271 self.result.append('Unknown fingerprint: {0}\nNo action taken.'.format(fpr_hash))
274 self.log.log(['dm-remove', fpr.fingerprint])
277 for entry in session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr):
278 self.log.log(['dm-remove', fpr.fingerprint, 'source={0}'.format(entry.source)])
280 session.delete(entry)
282 self.result.append('Removed: {0}.\n{1} acl entries removed.'.format(fpr.fingerprint, count))
286 def action_dm_migrate(self, fingerprint, section, session):
287 self._action_dm_admin_common(fingerprint, section, session)
289 acl_name = cnf.get('Command::DM::ACL', 'dm')
290 acl = session.query(ACL).filter_by(name=acl_name).one()
292 fpr_hash_from = section['From'].translate(None, ' ')
293 fpr_from = session.query(Fingerprint).filter_by(fingerprint=fpr_hash_from).first()
295 self.result.append('Unknown fingerprint (From): {0}\nNo action taken.'.format(fpr_hash_from))
298 fpr_hash_to = section['To'].translate(None, ' ')
299 fpr_to = session.query(Fingerprint).filter_by(fingerprint=fpr_hash_to).first()
301 self.result.append('Unknown fingerprint (To): {0}\nNo action taken.'.format(fpr_hash_to))
303 if fpr_to.keyring is None or fpr_to.keyring.keyring_name not in cnf.value_list('Command::DM::Keyrings'):
304 self.result.append('Key (To) {0} is not in DM keyring.\nNo action taken.'.format(fpr_to.fingerprint))
307 self.log.log(['dm-migrate', 'from={0}'.format(fpr_hash_from), 'to={0}'.format(fpr_hash_to)])
310 for entry in session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr_from):
311 self.log.log(['dm-migrate', 'from={0}'.format(fpr_hash_from), 'to={0}'.format(fpr_hash_to), 'source={0}'.format(entry.source)])
312 entry.fingerprint = fpr_to
315 self.result.append('Migrated {0} to {1}.\n{2} acl entries changed.'.format(fpr_hash_from, fpr_hash_to, count))
319 def action_break_the_archive(self, fingerprint, section, session):
321 uid = fingerprint.uid
322 if uid is not None and uid.name is not None:
323 name = uid.name.split()[0]
325 self.result.append("DAK9000: I'm sorry, {0}. I'm afraid I can't do that.".format(name))