]> git.decadent.org.uk Git - dak.git/blobdiff - daklib/checks.py
Allow timestamps in binary packages starting from 1975
[dak.git] / daklib / checks.py
index de180941eba05bdd9b7006f1f07eac7f77cd9361..3e7a73d4fdc2b2797bef281a298c9d3c86e5b3cc 100644 (file)
@@ -30,10 +30,12 @@ from daklib.regexes import *
 from daklib.textutils import fix_maintainer, ParseMaintError
 import daklib.lintian as lintian
 import daklib.utils as utils
+from daklib.upload import InvalidHashException
 
 import apt_inst
 import apt_pkg
 from apt_pkg import version_compare
+import errno
 import os
 import time
 import yaml
@@ -45,6 +47,21 @@ class Reject(Exception):
     """exception raised by failing checks"""
     pass
 
+class RejectStupidMaintainerException(Exception):
+    """exception raised by failing the external hashes check"""
+
+    def __str__(self):
+        return "'%s' has mismatching %s from the external files db ('%s' [current] vs '%s' [external])" % self.args[:4]
+
+class RejectACL(Reject):
+    """exception raise by failing ACL checks"""
+    def __init__(self, acl, reason):
+        self.acl = acl
+        self.reason = reason
+
+    def __str__(self):
+        return "ACL {0}: {1}".format(self.acl.name, self.reason)
+
 class Check(object):
     """base class for checks
 
@@ -82,7 +99,7 @@ class Check(object):
         """
         return False
 
-class SignatureCheck(Check):
+class SignatureAndHashesCheck(Check):
     """Check signature of changes and dsc file (if included in upload)
 
     Make sure the signature is valid and done by a known user.
@@ -91,14 +108,47 @@ class SignatureCheck(Check):
         changes = upload.changes
         if not changes.valid_signature:
             raise Reject("Signature for .changes not valid.")
-        if changes.source is not None:
-            if not changes.source.valid_signature:
+        self._check_hashes(upload, changes.filename, changes.files.itervalues())
+
+        source = None
+        try:
+            source = changes.source
+        except Exception as e:
+            raise Reject("Invalid dsc file: {0}".format(e))
+        if source is not None:
+            if not source.valid_signature:
                 raise Reject("Signature for .dsc not valid.")
-            if changes.source.primary_fingerprint != changes.primary_fingerprint:
+            if source.primary_fingerprint != changes.primary_fingerprint:
                 raise Reject(".changes and .dsc not signed by the same key.")
+            self._check_hashes(upload, source.filename, source.files.itervalues())
+
         if upload.fingerprint is None or upload.fingerprint.uid is None:
             raise Reject(".changes signed by unknown key.")
 
+    """Make sure hashes match existing files
+
+    @type  upload: L{daklib.archive.ArchiveUpload}
+    @param upload: upload we are processing
+
+    @type  filename: str
+    @param filename: name of the file the expected hash values are taken from
+
+    @type  files: sequence of L{daklib.upload.HashedFile}
+    @param files: files to check the hashes for
+    """
+    def _check_hashes(self, upload, filename, files):
+        try:
+            for f in files:
+                f.check(upload.directory)
+        except IOError as e:
+            if e.errno == errno.ENOENT:
+                raise Reject('{0} refers to non-existing file: {1}\n'
+                             'Perhaps you need to include it in your upload?'
+                             .format(filename, os.path.basename(e.filename)))
+            raise
+        except InvalidHashException as e:
+            raise Reject('{0}: {1}'.format(filename, unicode(e)))
+
 class ChangesCheck(Check):
     """Check changes file for syntax errors."""
     def check(self, upload):
@@ -156,16 +206,42 @@ class ChangesCheck(Check):
 
         return True
 
-class HashesCheck(Check):
-    """Check hashes in .changes and .dsc are valid."""
+class ExternalHashesCheck(Check):
+    """Checks hashes in .changes and .dsc against an external database."""
+    def check_single(self, session, f):
+        q = session.execute("SELECT size, md5sum, sha1sum, sha256sum FROM external_files WHERE filename LIKE '%%/%s'" % f.filename)
+        (ext_size, ext_md5sum, ext_sha1sum, ext_sha256sum) = q.fetchone() or (None, None, None, None)
+
+        if not ext_size:
+            return
+
+        if ext_size != f.size:
+            raise RejectStupidMaintainerException(f.filename, 'size', f.size, ext_size)
+
+        if ext_md5sum != f.md5sum:
+            raise RejectStupidMaintainerException(f.filename, 'md5sum', f.md5sum, ext_md5sum)
+
+        if ext_sha1sum != f.sha1sum:
+            raise RejectStupidMaintainerException(f.filename, 'sha1sum', f.sha1sum, ext_sha1sum)
+
+        if ext_sha256sum != f.sha256sum:
+            raise RejectStupidMaintainerException(f.filename, 'sha256sum', f.sha256sum, ext_sha256sum)
+
     def check(self, upload):
+        cnf = Config()
+
+        if not cnf.use_extfiles:
+            return
+
+        session = upload.session
         changes = upload.changes
+
         for f in changes.files.itervalues():
-            f.check(upload.directory)
-            source = changes.source
+            self.check_single(session, f)
+        source = changes.source
         if source is not None:
             for f in source.files.itervalues():
-                f.check(upload.directory)
+                self.check_single(session, f)
 
 class BinaryCheck(Check):
     """Check binary packages for syntax errors."""
@@ -252,7 +328,7 @@ class BinaryTimestampCheck(Check):
     def check(self, upload):
         cnf = Config()
         future_cutoff = time.time() + cnf.find_i('Dinstall::FutureTimeTravelGrace', 24*3600)
-        past_cutoff = time.mktime(time.strptime(cnf.find('Dinstall::PastCutoffYear', '1984'), '%Y'))
+        past_cutoff = time.mktime(time.strptime(cnf.find('Dinstall::PastCutoffYear', '1975'), '%Y'))
 
         class TarTime(object):
             def __init__(self):
@@ -260,9 +336,9 @@ class BinaryTimestampCheck(Check):
                 self.past_files = dict()
             def callback(self, member, data):
                 if member.mtime > future_cutoff:
-                    future_files[member.name] = member.mtime
+                    self.future_files[member.name] = member.mtime
                 elif member.mtime < past_cutoff:
-                    past_files[member.name] = member.mtime
+                    self.past_files[member.name] = member.mtime
 
         def format_reason(filename, direction, files):
             reason = "{0}: has {1} file(s) with a timestamp too far in the {2}:\n".format(filename, len(files), direction)
@@ -299,7 +375,10 @@ class SourceCheck(Check):
 
         version = control['Version']
         if is_orig:
-            version = re_field_version_upstream.match(version).group('upstream')
+            upstream_match = re_field_version_upstream.match(version)
+            if not upstream_match:
+                raise Reject('{0}: Source package includes upstream tarball, but {0} has no Debian revision.'.format(filename, version))
+            version = upstream_match.group('upstream')
         version_match =  re_field_version.match(version)
         version_without_epoch = version_match.group('without_epoch')
         if match.group('version') != version_without_epoch:
@@ -352,113 +431,100 @@ class SingleDistributionCheck(Check):
 
 class ACLCheck(Check):
     """Check the uploader is allowed to upload the packages in .changes"""
-    def _check_dm(self, upload):
-        # This code is not very nice, but hopefully works until we can replace
-        # DM-Upload-Allowed, cf. https://lists.debian.org/debian-project/2012/06/msg00029.html
-        session = upload.session
-
-        if 'source' not in upload.changes.architectures:
-            raise Reject('DM uploads must include source')
-        for f in upload.changes.files.itervalues():
-            if f.section == 'byhand' or f.section[:4] == "raw-":
-                raise Reject("Uploading byhand packages is not allowed for DMs.")
-
-        # Reject NEW packages
-        distributions = upload.changes.distributions
-        assert len(distributions) == 1
-        suite = session.query(Suite).filter_by(suite_name=distributions[0]).one()
-        overridesuite = suite
-        if suite.overridesuite is not None:
-            overridesuite = session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
-        if upload._check_new(overridesuite):
-            raise Reject('Uploading NEW packages is not allowed for DMs.')
-
-        # Check DM-Upload-Allowed
-        last_suites = ['unstable', 'experimental']
-        if suite.suite_name.endswith('-backports'):
-            last_suites = [suite.suite_name]
-        last = session.query(DBSource).filter_by(source=upload.changes.changes['Source']) \
-            .join(DBSource.suites).filter(Suite.suite_name.in_(last_suites)) \
-            .order_by(DBSource.version.desc()).limit(1).first()
-        if last is None:
-            raise Reject('No existing source found in {0}'.format(' or '.join(last_suites)))
-        if not last.dm_upload_allowed:
-            raise Reject('DM-Upload-Allowed is not set in {0}={1}'.format(last.source, last.version))
-
-        # check current Changed-by is in last Maintainer or Uploaders
-        uploader_names = [ u.name for u in last.uploaders ]
-        changed_by_field = upload.changes.changes.get('Changed-By', upload.changes.changes['Maintainer'])
-        if changed_by_field not in uploader_names:
-            raise Reject('{0} is not an uploader for {1}={2}'.format(changed_by_field, last.source, last.version))
-
-        # check Changed-by is the DM
-        changed_by = fix_maintainer(changed_by_field)
-        uid = upload.fingerprint.uid
-        if uid is None:
-            raise Reject('Unknown uid for fingerprint {0}'.format(upload.fingerprint.fingerprint))
-        if uid.uid != changed_by[3] and uid.name != changed_by[2]:
-            raise Reject('DMs are not allowed to sponsor uploads (expected {0} <{1}> as maintainer, but got {2})'.format(uid.name, uid.uid, changed_by_field))
 
+    def _does_hijack(self, session, upload, suite):
         # Try to catch hijacks.
         # This doesn't work correctly. Uploads to experimental can still
         # "hijack" binaries from unstable. Also one can hijack packages
         # via buildds (but people who try this should not be DMs).
         for binary_name in upload.changes.binary_names:
             binaries = session.query(DBBinary).join(DBBinary.source) \
-                .join(DBBinary.suites).filter(Suite.suite_name.in_(upload.changes.distributions)) \
+                .filter(DBBinary.suites.contains(suite)) \
                 .filter(DBBinary.package == binary_name)
             for binary in binaries:
                 if binary.source.source != upload.changes.changes['Source']:
-                    raise Reject('DMs must not hijack binaries (binary={0}, other-source={1})'.format(binary_name, binary.source.source))
-
-        return True
+                    return True, binary.package, binary.source.source
+        return False, None, None
+
+    def _check_acl(self, session, upload, acl):
+        source_name = upload.changes.source_name
+
+        if acl.match_fingerprint and upload.fingerprint not in acl.fingerprints:
+            return None, None
+        if acl.match_keyring is not None and upload.fingerprint.keyring != acl.match_keyring:
+            return None, None
+
+        if not acl.allow_new:
+            if upload.new:
+                return False, "NEW uploads are not allowed"
+            for f in upload.changes.files.itervalues():
+                if f.section == 'byhand' or f.section.startswith("raw-"):
+                    return False, "BYHAND uploads are not allowed"
+        if not acl.allow_source and upload.changes.source is not None:
+            return False, "sourceful uploads are not allowed"
+        binaries = upload.changes.binaries
+        if len(binaries) != 0:
+            if not acl.allow_binary:
+                return False, "binary uploads are not allowed"
+            if upload.changes.source is None and not acl.allow_binary_only:
+                return False, "binary-only uploads are not allowed"
+            if not acl.allow_binary_all:
+                uploaded_arches = set(upload.changes.architectures)
+                uploaded_arches.discard('source')
+                allowed_arches = set(a.arch_string for a in acl.architectures)
+                forbidden_arches = uploaded_arches - allowed_arches
+                if len(forbidden_arches) != 0:
+                    return False, "uploads for architecture(s) {0} are not allowed".format(", ".join(forbidden_arches))
+        if not acl.allow_hijack:
+            for suite in upload.final_suites:
+                does_hijack, hijacked_binary, hijacked_from = self._does_hijack(session, upload, suite)
+                if does_hijack:
+                    return False, "hijacks are not allowed (binary={0}, other-source={1})".format(hijacked_binary, hijacked_from)
+
+        acl_per_source = session.query(ACLPerSource).filter_by(acl=acl, fingerprint=upload.fingerprint, source=source_name).first()
+        if acl.allow_per_source:
+            if acl_per_source is None:
+                return False, "not allowed to upload source package '{0}'".format(source_name)
+        if acl.deny_per_source and acl_per_source is not None:
+            return False, acl_per_source.reason or "forbidden to upload source package '{0}'".format(source_name)
+
+        return True, None
 
     def check(self, upload):
+        session = upload.session
         fingerprint = upload.fingerprint
-        source_acl = fingerprint.source_acl
-        if source_acl is None:
-            if 'source' in upload.changes.architectures:
-                raise Reject('Fingerprint {0} must not upload source'.format(fingerprint.fingerprint))
-        elif source_acl.access_level == 'dm':
-            self._check_dm(upload)
-        elif source_acl.access_level != 'full':
-            raise Reject('Unknown source_acl access level {0} for fingerprint {1}'.format(source_acl.access_level, fingerprint.fingerprint))
-
-        bin_architectures = set(upload.changes.architectures)
-        bin_architectures.discard('source')
-        binary_acl = fingerprint.binary_acl
-        if binary_acl is None:
-            if len(bin_architectures) > 0:
-                raise Reject('Fingerprint {0} must not upload binary packages'.format(fingerprint.fingerprint))
-        elif binary_acl.access_level == 'map':
-            query = upload.session.query(BinaryACLMap).filter_by(fingerprint=fingerprint)
-            allowed_architectures = [ m.architecture.arch_string for m in query ]
-
-            for arch in upload.changes.architectures:
-                if arch not in allowed_architectures:
-                    raise Reject('Fingerprint {0} must not  upload binaries for architecture {1}'.format(fingerprint.fingerprint, arch))
-        elif binary_acl.access_level != 'full':
-            raise Reject('Unknown binary_acl access level {0} for fingerprint {1}'.format(binary_acl.access_level, fingerprint.fingerprint))
+        keyring = fingerprint.keyring
 
-        return True
+        if keyring is None:
+            raise Reject('No keyring for fingerprint {0}'.format(fingerprint.fingerprint))
+        if not keyring.active:
+            raise Reject('Keyring {0} is not active'.format(keyring.name))
 
-class UploadBlockCheck(Check):
-    """check for upload blocks"""
-    def check(self, upload):
-        session = upload.session
-        control = upload.changes.changes
+        acl = fingerprint.acl or keyring.acl
+        if acl is None:
+            raise Reject('No ACL for fingerprint {0}'.format(fingerprint.fingerprint))
+        result, reason = self._check_acl(session, upload, acl)
+        if not result:
+            raise RejectACL(acl, reason)
 
-        source = re_field_source.match(control['Source']).group('package')
-        version = control['Version']
-        blocks = session.query(UploadBlock).filter_by(source=source) \
-                        .filter((UploadBlock.version == version) | (UploadBlock.version == None))
+        for acl in session.query(ACL).filter_by(is_global=True):
+            result, reason = self._check_acl(session, upload, acl)
+            if result == False:
+                raise RejectACL(acl, reason)
 
-        for block in blocks:
-            if block.fingerprint == upload.fingerprint:
-                raise Reject('Manual upload block in place for package {0} and fingerprint {1}:\n{2}'.format(source, upload.fingerprint.fingerprint, block.reason))
-            if block.uid == upload.fingerprint.uid:
-                raise Reject('Manual upload block in place for package {0} and uid {1}:\n{2}'.format(source, block.uid.uid, block.reason))
+        return True
 
+    def per_suite_check(self, upload, suite):
+        acls = suite.acls
+        if len(acls) != 0:
+            accept = False
+            for acl in acls:
+                result, reason = self._check_acl(upload.session, upload, acl)
+                if result == False:
+                    raise Reject(reason)
+                accept = accept or result
+            if not accept:
+                raise Reject('Not accepted by any per-suite acl (suite={0})'.format(suite.suite_name))
         return True
 
 class TransitionCheck(Check):
@@ -575,11 +641,14 @@ class LintianCheck(Check):
 
         changespath = os.path.join(upload.directory, changes.filename)
         try:
-            if cnf.unpribgroup:
-                cmd = "sudo -H -u {0} -- /usr/bin/lintian --show-overrides --tags-from-file {1} {2}".format(cnf.unprivgroup, temp_filename, changespath)
-            else:
-                cmd = "/usr/bin/lintian --show-overrides --tags-from-file {0} {1}".format(temp_filename, changespath)
-            result, output = commands.getstatusoutput(cmd)
+            cmd = []
+
+            user = cnf.get('Dinstall::UnprivUser') or None
+            if user is not None:
+                cmd.extend(['sudo', '-H', '-u', user])
+
+            cmd.extend(['LINTIAN_COLL_UNPACKED_SKIP_SIG=1', '/usr/bin/lintian', '--show-overrides', '--tags-from-file', temp_filename, changespath])
+            result, output = commands.getstatusoutput(" ".join(cmd))
         finally:
             os.unlink(temp_filename)
 
@@ -615,7 +684,7 @@ class SuiteArchitectureCheck(Check):
         for arch in upload.changes.architectures:
             query = session.query(Architecture).filter_by(arch_string=arch).filter(Architecture.suites.contains(suite))
             if query.first() is None:
-                raise Reject('Architecture {0} is not allowed in suite {2}'.format(arch, suite.suite_name))
+                raise Reject('Architecture {0} is not allowed in suite {1}'.format(arch, suite.suite_name))
 
         return True