]> git.decadent.org.uk Git - dak.git/blob - daklib/command.py
Merge branch 'new-dm'
[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, path, 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.path = path
46         self.filename = os.path.basename(path)
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 _evaluate_sections(self, sections, session):
63         session.rollback()
64         try:
65             sections.next()
66             section = sections.section
67
68             action = section.get('Action', None)
69             if action is None:
70                 raise CommandError('Encountered section without Action field')
71             self.result.append('Action: {0}'.format(action))
72
73             if action == 'dm':
74                 self.action_dm(self.fingerprint, section, session)
75             else:
76                 raise CommandError('Unknown action: {0}'.format(action))
77         except StopIteration:
78             pass
79         finally:
80             session.rollback()
81
82     def _notify_uploader(self):
83         cnf = Config()
84
85         bcc = 'X-DAK: dak process-command'
86         if 'Dinstall::Bcc' in cnf:
87             bcc = '{0}\nBcc: {1}'.format(bcc, cnf['Dinstall::Bcc'])
88
89         cc = set(fix_maintainer(address)[1] for address in self.cc)
90
91         subst = {
92             '__DAK_ADDRESS__': cnf['Dinstall::MyEmailAddress'],
93             '__MAINTAINER_TO__': fix_maintainer(self.uploader)[1],
94             '__CC__': ", ".join(cc),
95             '__BCC__': bcc,
96             '__RESULTS__': "\n".join(self.result),
97             '__FILENAME__': self.filename,
98             }
99
100         message = TemplateSubst(subst, os.path.join(cnf['Dir::Templates'], 'process-command.processed'))
101
102         send_mail(message)
103
104     def evaluate(self):
105         """evaluate commands file
106
107         @rtype:   bool
108         @returns: C{True} if the file was processed sucessfully,
109                   C{False} otherwise
110         """
111         result = True
112
113         session = DBConn().session()
114
115         keyrings = session.query(Keyring).filter_by(active=True).order_by(Keyring.priority)
116         keyring_files = [ k.keyring_name for k in keyrings ]
117
118         raw_contents = open(self.path, 'r').read()
119         signed_file = SignedFile(raw_contents, keyring_files)
120         if not signed_file.valid:
121             self.log.log(['invalid signature', self.filename])
122             return False
123
124         self.fingerprint = session.query(Fingerprint).filter_by(fingerprint=signed_file.primary_fingerprint).one()
125         if self.fingerprint.keyring is None:
126             self.log.log(['singed by key in unknown keyring', self.filename])
127             return False
128         assert self.fingerprint.keyring.active
129
130         self.log.log(['processing', self.filename, 'signed-by={0}'.format(self.fingerprint.fingerprint)])
131
132         with tempfile.TemporaryFile() as fh:
133             fh.write(signed_file.contents)
134             fh.seek(0)
135             sections = apt_pkg.TagFile(fh)
136
137         self.uploader = None
138         addresses = gpg_get_key_addresses(self.fingerprint.fingerprint)
139         if len(addresses) > 0:
140             self.uploader = addresses[0]
141
142         try:
143             sections.next()
144             section = sections.section
145             if 'Uploader' in section:
146                 self.uploader = section['Uploader']
147             # TODO: Verify first section has valid Archive field
148             if 'Archive' not in section:
149                 raise CommandError('No Archive field in first section.')
150
151             # TODO: send mail when we detected a replay.
152             self._check_replay(signed_file, session)
153
154             self._evaluate_sections(sections, session)
155             self.result.append('')
156         except Exception as e:
157             self.log.log(['ERROR', e])
158             self.result.append("There was an error processing this section:\n{0}".format(e))
159             result = False
160
161         self._notify_uploader()
162
163         session.close()
164         self.log.log(['done', self.filename])
165
166         return result
167
168     def _split_packages(self, value):
169         names = value.split()
170         for name in names:
171             if not re_field_package.match(name):
172                 raise CommandError('Invalid package name "{0}"'.format(name))
173         return names
174
175     def action_dm(self, fingerprint, section, session):
176         cnf = Config()
177
178         if 'Command::DM::AdminKeyrings' not in cnf \
179                 or 'Command::DM::ACL' not in cnf \
180                 or 'Command::DM::Keyrings' not in cnf:
181             raise CommandError('DM command is not configured for this archive.')
182
183         allowed_keyrings = cnf.value_list('Command::DM::AdminKeyrings')
184         if fingerprint.keyring.keyring_name not in allowed_keyrings:
185             raise CommandError('Key {0} is not allowed to set DM'.format(fingerprint.fingerprint))
186
187         acl_name = cnf.get('Command::DM::ACL', 'dm')
188         acl = session.query(ACL).filter_by(name=acl_name).one()
189
190         fpr = session.query(Fingerprint).filter_by(fingerprint=section['Fingerprint']).one()
191         if fpr.keyring is None or fpr.keyring.keyring_name not in cnf.value_list('Command::DM::Keyrings'):
192             raise CommandError('Key {0} is not in DM keyring.'.format(fpr.fingerprint))
193         addresses = gpg_get_key_addresses(fpr.fingerprint)
194         if len(addresses) > 0:
195             self.cc.append(addresses[0])
196
197         self.log.log(['dm', 'fingerprint', fpr.fingerprint])
198         self.result.append('Fingerprint: {0}'.format(fpr.fingerprint))
199         if len(addresses) > 0:
200             self.log.log(['dm', 'uid', addresses[0]])
201             self.result.append('Uid: {0}'.format(addresses[0]))
202
203         for source in self._split_packages(section.get('Allow', '')):
204             if session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr, source=source).first() is None:
205                 aps = ACLPerSource()
206                 aps.acl = acl
207                 aps.fingerprint = fpr
208                 aps.source = source
209                 session.add(aps)
210                 self.log.log(['dm', 'allow', fpr.fingerprint, source])
211                 self.result.append('Allowed: {0}'.format(source))
212             else:
213                 self.result.append('Already-Allowed: {0}'.format(source))
214
215         session.flush()
216
217         for source in self._split_packages(section.get('Deny', '')):
218             session.query(ACLPerSource).filter_by(acl=acl, fingerprint=fpr, source=source).delete()
219             self.log.log(['dm', 'deny', fpr.fingerprint, source])
220             self.result.append('Denied: {0}'.format(source))
221
222         session.commit()