X-Git-Url: https://git.decadent.org.uk/gitweb/?a=blobdiff_plain;f=daklib%2Fchecks.py;h=7298026208a82a8f231812b253a581f7c9b5d2f0;hb=c4d894e1b37e4dceb62395fb4b1c30d273b7a2e7;hp=6091bc73f60deca61c50a74155022fd0bd475f45;hpb=09a1a20566dcf84ca229b4339bd8f8080eb59afd;p=dak.git diff --git a/daklib/checks.py b/daklib/checks.py index 6091bc73..72980262 100644 --- a/daklib/checks.py +++ b/daklib/checks.py @@ -34,6 +34,7 @@ import daklib.utils as utils import apt_inst import apt_pkg from apt_pkg import version_compare +import errno import os import time import yaml @@ -45,6 +46,12 @@ 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 Check(object): """base class for checks @@ -159,13 +166,60 @@ class ChangesCheck(Check): class HashesCheck(Check): """Check hashes in .changes and .dsc are valid.""" def check(self, upload): + what = None + try: + changes = upload.changes + what = changes.filename + for f in changes.files.itervalues(): + f.check(upload.directory) + source = changes.source + what = source.filename + if source is not None: + for f in source.files.itervalues(): + 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(what, os.path.basename(e.filename))) + raise + +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.""" @@ -352,28 +406,80 @@ class SingleDistributionCheck(Check): class ACLCheck(Check): """Check the uploader is allowed to upload the packages in .changes""" - def _check_dm(self, upload): + + 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) \ + .filter(DBBinary.suites.contains(suite)) \ + .filter(DBBinary.package == binary_name) + for binary in binaries: + if binary.source.source != upload.changes.changes['Source']: + return True, binary, 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: + # XXX: Drop DMUA part here and switch to new implementation. + # XXX: Send warning mail once users can set the new DMUA flag + dmua_status, dmua_reason = self._check_dmua(upload) + if not dmua_status: + return False, dmua_reason + #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_dmua(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 + suites = upload.final_suites + assert len(suites) == 1 + suite = list(suites)[0] + last_suites = ['unstable', 'experimental'] if suite.suite_name.endswith('-backports'): last_suites = [suite.suite_name] @@ -381,84 +487,61 @@ class ACLCheck(Check): .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))) + return False, '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)) + return False, '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)) + return False, '{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)) + return False, '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)) - - # 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.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 False, 'DMs are not allowed to sponsor uploads (expected {0} <{1}> as maintainer, but got {2})'.format(uid.name, uid.uid, changed_by_field) - return True + 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 Reject(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 Reject(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): @@ -471,6 +554,7 @@ class TransitionCheck(Check): if transitions is None: return True + control = upload.changes.changes source = re_field_source.match(control['Source']).group('package') for trans in transitions: @@ -565,7 +649,7 @@ class LintianCheck(Check): except yaml.YAMLError as msg: raise Exception('Could not read lintian tags file {0}, YAML error: {1}'.format(tagfile, msg)) - fd, temp_filename = utils.temp_filename() + fd, temp_filename = utils.temp_filename(mode=0o644) temptagfile = os.fdopen(fd, 'w') for tags in lintiantags.itervalues(): for tag in tags: @@ -574,8 +658,10 @@ class LintianCheck(Check): changespath = os.path.join(upload.directory, changes.filename) try: - # FIXME: no shell - cmd = "lintian --show-overrides --tags-from-file {0} {1}".format(temp_filename, changespath) + if cnf.unprivgroup: + 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) finally: os.unlink(temp_filename)