Repository: fedora-infra/bodhi

Issue:
[SECURITY ISSUE] Why can other people push my packages?
The permissions seem buggered on the new bodhi

https://bodhi.fedoraproject.org/updates/tint2-0.12.2-1.fc22

thofmann pushed tint2 to testing (he has no perms for this action)

https://bodhi.fedoraproject.org/updates/tint2-0.12.2-1.fc22#comment-313306

In return I unpushed (I have no right or perms to do so).

https://bodhi.fedoraproject.org/updates/parcimonie.sh-0-0.4.20150804gitc009937.el7



Discussion:
I confirm: I can push and revoke updates without having the right permissions, see https://lists.fedoraproject.org/pipermail/devel/2015-August/213639.html


Resolving patch:
diff --git a/bodhi/validators.py b/bodhi/validators.py
--- a/bodhi/validators.py
+++ b/bodhi/validators.py
@@ -41,13 +41,20 @@ def validate_csrf_token(node, value):
         raise colander.Invalid(node, 'CSRF tokens do not match')
 
 
+def cache_nvrs(request, build):
+    if build in request.buildinfo and 'nvr' in request.buildinfo[build]:
+        return
+    if not build in request.buildinfo:
+        request.buildinfo[build] = {}
+    name, version, release = get_nvr(build)
+    request.buildinfo[build]['nvr'] = name, version, release
+    if '' in (name, version, release):
+        raise ValueError
+
 def validate_nvrs(request):
     for build in request.validated.get('builds', []):
         try:
-            name, version, release = get_nvr(build)
-            request.buildinfo[build]['nvr'] = name, version, release
-            if '' in (name, version, release):
-                raise ValueError
+            cache_nvrs(request, build)
         except:
             request.validated['builds'] = []
             request.errors.add('body', 'builds', 'Build not in '
@@ -103,6 +110,7 @@ def validate_build_tags(request):
                          .release
     else:
         valid_tags = tag_types['candidate']
+
     for build in request.validated.get('builds', []):
         valid = False
         try:
@@ -116,7 +124,14 @@ def validate_build_tags(request):
 
         # Disallow adding builds for a different release
         if edited:
-            build_rel = Release.from_tags(tags, request.db)
+            try:
+                build_rel = Release.from_tags(tags, request.db)
+            except KeyError:
+                msg = 'Cannot find release associated with build: {}, tags: {}'.format(build, tags)
+                log.warn(msg)
+                request.errors.add('body', 'builds', msg)
+                return
+
             if build_rel is not release:
                 request.errors.add('body', 'builds',
                         'Cannot add a %s build to an %s update' %
@@ -160,43 +175,116 @@ def validate_acls(request):
     groups = []
     notify_groups = []
 
-    for build in request.validated.get('builds', []):
-        buildinfo = request.buildinfo[build]
+    # There are two different code-paths that could pass through this validator
+    # One of them is for submitting something new with a list of builds (a new
+    # update, or editing an update by changing the list of builds).  The other
+    # is for changing the request on an existing update -- where an update
+    # title has been passed to us, but not a list of builds.
+    # We need to validate that the user has commit rights on all the build
+    # (either the explicitly listed ones or on the ones associated with the
+    # pre-existing update).. and so we'll do some conditional branching below
+    # to handle those two scenarios.
+
+    # This can get confusing.
+    # TODO -- we should break these two roles out into two different clearly
+    # defined validator functions like `validate_acls_for_builds` and
+    # `validate_acls_for_update`.
+
+    builds = None
+    if 'builds' in request.validated:
+        builds = request.validated['builds']
+
+    if 'update' in request.validated:
+        builds = request.validated['update'].builds
+
+    if not builds:
+        log.error("validate_acls was passed data with nothing to validate.")
+        request.errors.add('body', 'builds', 'ACL validation mechanism was '
+                           'unable to determine ACLs.')
+        return
 
-        # Get the Package object
-        package_name = buildinfo['nvr'][0]
-        package = db.query(Package).filter_by(name=package_name).first()
-        if not package:
-            package = Package(name=package_name)
-            db.add(package)
-            db.flush()
+    for build in builds:
+        # The whole point of the blocks inside this conditional is to determine
+        # the "release" and "package" associated with the given build.  For raw
+        # (new) builds, we have to do that by hand.  For builds that have been
+        # previously associated with an update, we can just look it up no prob.
+        if 'builds' in request.validated:
+            # Split out NVR data unless its already done.
+            try:
+                cache_nvrs(request, build)
+            except ValueError:
+                error = 'Problem caching NVRs when validating ACLs.'
+                log.exception(error)
+                request.errors.add('body', 'builds', error)
+                return
 
-        # Determine the release associated with this build
-        tags = buildinfo['tags']
-        try:
-            release = Release.from_tags(tags, db)
-        except KeyError:
-            log.exception('Unable to determine release from tags')
-            request.errors.add('body', 'builds', 'Unable to determine release ' +
-                               'from build: %s' % build)
-            return
-        buildinfo['release'] = release
-        if not release:
-            msg = 'Cannot find release associated with tags: {}'.format(tags)
-            log.warn(msg)
-            request.errors.add('body', 'builds', msg)
-            return
+            buildinfo = request.buildinfo[build]
+
+            # Get the Package object
+            package_name = buildinfo['nvr'][0]
+            package = db.query(Package).filter_by(name=package_name).first()
+            if not package:
+                package = Package(name=package_name)
+                db.add(package)
+                db.flush()
+
+            # Determine the release associated with this build
+            tags = buildinfo.get('tags', [])
+            try:
+                release = Release.from_tags(tags, db)
+            except KeyError:
+                log.exception('Unable to determine release from tags')
+                request.errors.add('body', 'builds',
+                                   'Unable to determine release ' +
+                                   'from build: %s' % build)
+                return
+            buildinfo['release'] = release
+            if not release:
+                msg = 'Cannot find release associated with ' + \
+                    'build: {}, tags: {}'.format(build, tags)
+                log.warn(msg)
+                request.errors.add('body', 'builds', msg)
+                return
+        elif 'update' in request.validated:
+            buildinfo = request.buildinfo[build.nvr]
+
+            # Easy to find the release and package since they're associated
+            # with a pre-stored Build obj.
+            package = build.package
+            release = build.update.release
+
+            # Some sanity checking..
+            if not package:
+                msg = build.nvr + ' has no package associated with it in ' + \
+                    'our DB, so we cannot verify that you\'re in the ACL.'
+                log.error(msg)
+                request.errors.add('body', 'builds', msg)
+                return
+            if not release:
+                msg = build.nvr + ' has no release associated with it in ' + \
+                    'our DB, so we cannot verify that you\'re in the ACL.'
+                log.error(msg)
+                request.errors.add('body', 'builds', msg)
+                return
+        else:
+            raise NotImplementedError()  # Should never get here.
+
+        # Now that we know the release and the package associated with this
+        # build, we can ask our ACL system about it..
 
         acl_system = settings.get('acl_system')
         if acl_system == 'pkgdb':
             try:
-                people, groups = package.get_pkg_pushers(release.branch, settings)
+                people, groups = package.get_pkg_pushers(
+                    release.branch, settings)
                 committers, watchers = people
                 groups, notify_groups = groups
             except Exception, e:
                 log.exception(e)
-                request.errors.add('body', 'builds', "Unable to access the Package "
-                                   "Database. Please try again later.")
+                request.errors.add('body', 'builds',
+                                   "Unable to access the Package "
+                                   "Database to check ACLs. Please "
+                                   "try again later.")
                 return
         elif acl_system == 'dummy':
             people = (['ralph', 'guest'], ['guest'])


Test patch:
diff --git a/bodhi/tests/functional/test_updates.py b/bodhi/tests/functional/test_updates.py
--- a/bodhi/tests/functional/test_updates.py
+++ b/bodhi/tests/functional/test_updates.py
@@ -94,10 +94,15 @@ def test_fail_on_edit_with_empty_build_list(self, *args):
         update['edited'] = update['builds']  # the update title..
         update['builds'] = []
         res = self.app.post_json('/updates/', update, status=400)
-        self.assertEquals(len(res.json_body['errors']), 1)
+        self.assertEquals(len(res.json_body['errors']), 2)
         self.assertEquals(res.json_body['errors'][0]['name'], 'builds')
-        self.assertEquals(res.json_body['errors'][0]['description'],
-                          'You may not specify an empty list of builds.')
+        self.assertEquals(
+            res.json_body['errors'][0]['description'],
+            'You may not specify an empty list of builds.')
+        self.assertEquals(res.json_body['errors'][1]['name'], 'builds')
+        self.assertEquals(
+            res.json_body['errors'][1]['description'],
+            'ACL validation mechanism was unable to determine ACLs.')
 
     @mock.patch(**mock_taskotron_results)
     @mock.patch(**mock_valid_requirements)
@@ -324,7 +329,7 @@ def test_pkgdb_outage(self, *args):
         update = self.get_update(u'bodhi-2.0-2.fc17')
         update['csrf_token'] = app.get('/csrf').json_body['csrf_token']
         res = app.post_json('/updates/', update, status=400)
-        assert "Unable to access the Package Database. Please try again later." in res, res
+        assert "Unable to access the Package Database" in res, res
 
     @mock.patch(**mock_valid_requirements)
     def test_invalid_acl_system(self, *args):