From b08d50f80db0813be96f9cfbf18b20834abcb558 Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Mar 26 2019 11:47:00 +0000
Subject: [PATCH 1/4] check architecture names for mistakes


Fixes: https://pagure.io/koji/issue/1237

---

diff --git a/hub/kojihub.py b/hub/kojihub.py
index bac7203..2591727 100644
--- a/hub/kojihub.py
+++ b/hub/kojihub.py
@@ -875,10 +875,11 @@ def _pkglist_remove(tag_id, pkg_id):
 
 def _pkglist_add(tag_id, pkg_id, owner, block, extra_arches):
     #revoke old entry (if present)
-    _pkglist_remove(tag_id, pkg_id)
     data = dslice(locals(), ('tag_id', 'owner', 'extra_arches'))
     data['package_id'] = pkg_id
     data['blocked'] = block
+    validate_arches_string(data['extra_arches'])
+    _pkglist_remove(tag_id, pkg_id)
     insert = InsertProcessor('tag_packages', data=data)
     insert.make_create()  #XXX user_id?
     insert.execute()
@@ -915,6 +916,8 @@ def _direct_pkglist_add(taginfo, pkginfo, owner, block, extra_arches, force,
             assert_policy('package_list', policy_data)
     if not pkg:
         pkg = lookup_package(pkginfo, create=True)
+    # validate arches before running callbacks
+    validate_arches_string(extra_arches)
     koji.plugin.run_callbacks('prePackageListChange', action=action, tag=tag, package=pkg, owner=owner,
                               block=block, extra_arches=extra_arches, force=force, update=update)
     # first check to see if package is:
@@ -2983,6 +2986,16 @@ def lookup_build_target(info, strict=False, create=False):
     return lookup_name('build_target', info, strict, create)
 
 
+def validate_arches_string(arches, strict=True):
+    """run checks against architectures space-separated strings"""
+    if re.match(r'^[a-zA-Z0-9_\- ]+$', arches):
+        return True
+    elif strict:
+        raise koji.GenericError("Architecture can be only [a-zA-Z0-9_-]")
+    else:
+        return False
+
+
 def create_tag(name, parent=None, arches=None, perm=None, locked=False, maven_support=False, maven_include_all=False, extra=None):
     """Create a new tag"""
     context.session.assertPerm('admin')
@@ -2997,6 +3010,9 @@ def _create_tag(name, parent=None, arches=None, perm=None, locked=False, maven_s
         raise koji.GenericError("Tag name %s is too long. Max length is %s characters",
                                 name, max_name_length)
 
+    if arches is not None:
+        validate_arches_string(arches)
+
     if not context.opts.get('EnableMaven') and (maven_support or maven_include_all):
         raise koji.GenericError("Maven support not enabled")
 
@@ -3177,6 +3193,11 @@ SET name = %(name)s
 WHERE id = %(tagID)i"""
         _dml(update, values)
 
+    # sanitize architecture names (space-separated string)
+    arches = kwargs.get('arches')
+    if arches and tag['arches'] != arches:
+        validate_arches_string(arches)
+
     #check for changes
     data = tag.copy()
     changed = False
diff --git a/tests/test_hub/test_create_tag.py b/tests/test_hub/test_create_tag.py
index f483211..95daddb 100644
--- a/tests/test_hub/test_create_tag.py
+++ b/tests/test_hub/test_create_tag.py
@@ -1,8 +1,6 @@
+# coding: utf-8
 from __future__ import absolute_import
-import copy
 import mock
-import shutil
-import tempfile
 try:
     import unittest2 as unittest
 except ImportError:
@@ -67,3 +65,20 @@ class TestCreateTag(unittest.TestCase):
         self.assertEqual(insert.data, values)
         self.assertEqual(insert.rawdata, {})
         insert = self.inserts[0]
+
+    def test_invalid_archs(self):
+        self.get_tag.return_value = None
+        self.get_tag_id.return_value = 99
+        self.context.event_id = 42
+        self.context.session.user_id = 23
+
+        with self.assertRaises(koji.GenericError):
+            kojihub.create_tag('newtag', arches=u'ěšč')
+
+        with self.assertRaises(koji.GenericError):
+            kojihub.create_tag('newtag', arches=u'arch1;arch2')
+
+        with self.assertRaises(koji.GenericError):
+            kojihub.create_tag('newtag', arches=u'arch1,arch2')
+
+        self.assertEqual(len(self.inserts), 0)
diff --git a/tests/test_hub/test_edit_tag.py b/tests/test_hub/test_edit_tag.py
index cb32059..e207074 100644
--- a/tests/test_hub/test_edit_tag.py
+++ b/tests/test_hub/test_edit_tag.py
@@ -1,3 +1,4 @@
+# coding: utf-8
 from __future__ import absolute_import
 import mock
 try:
@@ -197,3 +198,40 @@ WHERE id = %(tagID)i""", {'name': 'newtag', 'tagID': 333})
         self.get_perm_id.assert_called_once()
         self._singleValue.assert_called_once()
         self.assertEqual(cm.exception.args[0], 'Name newtag already taken by tag 2')
+
+    def test_invalid_archs(self):
+        self.get_tag.return_value = {
+            'create_event': 42,
+            'creator_id': 23,
+            'arches': 'arch1 arch2',
+            'locked': True,
+            'maven_include_all': True,
+            'maven_support': True,
+            'perm_id': None,
+            'tag_id': 333,
+            'name': 'newtag',
+            'id': 345,
+        }
+
+        # valid
+        kwargs = {
+            'name': 'newtag',
+            'arches': 'valid_arch',
+        }
+        kojihub._edit_tag('tag', **kwargs)
+
+        # invalid 1
+        kwargs['arches'] = u'ěšč'
+        with self.assertRaises(koji.GenericError):
+            kojihub._edit_tag('tag', **kwargs)
+
+        # invalid 2
+        kwargs['arches'] = u'arch1;arch2'
+        with self.assertRaises(koji.GenericError):
+            kojihub._edit_tag('tag', **kwargs)
+
+        # invalid 2
+        kwargs['arches'] = u'arch1,arch2'
+        with self.assertRaises(koji.GenericError):
+            kojihub._edit_tag('tag', **kwargs)
+
diff --git a/tests/test_hub/test_validate_arches_string.py b/tests/test_hub/test_validate_arches_string.py
new file mode 100644
index 0000000..9261421
--- /dev/null
+++ b/tests/test_hub/test_validate_arches_string.py
@@ -0,0 +1,25 @@
+# coding: utf-8
+from __future__ import absolute_import
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+import koji
+import kojihub
+
+class TestValidateArchesString(unittest.TestCase):
+    def test_valid_arches(self):
+        kojihub.validate_arches_string('i386')
+        kojihub.validate_arches_string('i386 x86_64')
+        kojihub.validate_arches_string('i386 x86_64   ')
+
+    def test_invalid_arches(self):
+        with self.assertRaises(koji.GenericError):
+            kojihub.validate_arches_string(u'ěšč')
+
+        with self.assertRaises(koji.GenericError):
+            kojihub.validate_arches_string(u'i386;x86_64')
+
+        with self.assertRaises(koji.GenericError):
+            kojihub.validate_arches_string(u'i386,x86_64')
+

From e0969f966ce7d137e9084a7faa4044b3d1cb4c4c Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Mar 26 2019 11:47:56 +0000
Subject: [PATCH 2/4] use same arch parsing for cli and hub


Related: https://pagure.io/koji/issue/1237

---

diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py
index 8efe4de..e51c08e 100644
--- a/cli/koji_cli/commands.py
+++ b/cli/koji_cli/commands.py
@@ -195,7 +195,7 @@ def handle_edit_host(options, session, args):
         if val is not None:
             vals[key] = val
     if 'arches' in vals:
-        vals['arches'] = parse_arches(vals['arches'])
+        vals['arches'] = koji.parse_arches(vals['arches'])
 
     session.multicall = True
     for host in args:
@@ -350,7 +350,7 @@ def handle_add_pkg(goptions, session, args):
             continue
         to_add.append(package)
     if options.extra_arches:
-        opts['extra_arches'] = parse_arches(options.extra_arches)
+        opts['extra_arches'] = koji.parse_arches(options.extra_arches)
 
     # add the packages
     print("Adding %i packages to tag %s" % (len(to_add), dsttag['name']))
@@ -472,7 +472,7 @@ def handle_build(options, session, args):
     source = args[1]
     opts = {}
     if build_opts.arch_override:
-        opts['arch_override'] = parse_arches(build_opts.arch_override)
+        opts['arch_override'] = koji.parse_arches(build_opts.arch_override)
     for key in ('skip_tag', 'scratch', 'repo_id', 'fail_fast'):
         val = getattr(build_opts, key)
         if val is not None:
@@ -4823,7 +4823,7 @@ def handle_add_tag(goptions, session, args):
     if options.parent:
         opts['parent'] = options.parent
     if options.arches:
-        opts['arches'] = parse_arches(options.arches)
+        opts['arches'] = koji.parse_arches(options.arches)
     if options.maven_support:
         opts['maven_support'] = True
     if options.include_all:
@@ -4865,7 +4865,7 @@ def handle_edit_tag(goptions, session, args):
     tag = args[0]
     opts = {}
     if options.arches:
-        opts['arches'] = parse_arches(options.arches)
+        opts['arches'] = koji.parse_arches(options.arches)
     if options.no_perm:
         opts['perm_id'] = None
     elif options.perm:
@@ -6237,7 +6237,7 @@ def handle_set_pkg_arches(goptions, session, args):
         parser.error(_("Please specify an archlist, a tag, and at least one package"))
         assert False  # pragma: no cover
     activate_session(session, goptions)
-    arches = parse_arches(args[0])
+    arches = koji.parse_arches(args[0])
     tag = args[1]
     for package in args[2:]:
         #really should implement multicall...
diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py
index 8e8bf99..5d77954 100644
--- a/cli/koji_cli/lib.py
+++ b/cli/koji_cli/lib.py
@@ -20,6 +20,8 @@ except ImportError:  # pragma: no cover
 
 import koji
 from koji.util import to_list
+# import parse_arches to current namespace for backward compatibility
+from koji import parse_arches
 
 # for compatibility with plugins based on older version of lib
 # Use optparse imports directly in new code.
@@ -132,26 +134,6 @@ def print_task_recurse(task,depth=0):
         print_task_recurse(child,depth+1)
 
 
-def parse_arches(arches, to_list=False):
-    """Normalize user input for a list of arches.
-
-    This method parses a single comma- or space-separated string of arches and
-    returns a space-separated string.
-
-    :param str arches: comma- or space-separated string of arches, eg.
-                       "x86_64,ppc64le", or "x86_64 ppc64le"
-    :param bool to_list: return a list of each arch, instead of a single
-                         string. This is False by default.
-    :returns: a space-separated string like "x86_64 ppc64le", or a list like
-              ['x86_64', 'ppc64le'].
-    """
-    arches = arches.replace(',', ' ').split()
-    if to_list:
-        return arches
-    else:
-        return ' '.join(arches)
-
-
 class TaskWatcher(object):
 
     def __init__(self,task_id,session,level=0,quiet=False):
diff --git a/hub/kojihub.py b/hub/kojihub.py
index 2591727..6477e84 100644
--- a/hub/kojihub.py
+++ b/hub/kojihub.py
@@ -878,7 +878,7 @@ def _pkglist_add(tag_id, pkg_id, owner, block, extra_arches):
     data = dslice(locals(), ('tag_id', 'owner', 'extra_arches'))
     data['package_id'] = pkg_id
     data['blocked'] = block
-    validate_arches_string(data['extra_arches'])
+    data['extra_arches'] = koji.parse_arches(data['extra_arches'], strict=True, allow_none=True)
     _pkglist_remove(tag_id, pkg_id)
     insert = InsertProcessor('tag_packages', data=data)
     insert.make_create()  #XXX user_id?
@@ -917,7 +917,7 @@ def _direct_pkglist_add(taginfo, pkginfo, owner, block, extra_arches, force,
     if not pkg:
         pkg = lookup_package(pkginfo, create=True)
     # validate arches before running callbacks
-    validate_arches_string(extra_arches)
+    extra_arches = koji.parse_arches(extra_arches, strict=True, allow_none=True)
     koji.plugin.run_callbacks('prePackageListChange', action=action, tag=tag, package=pkg, owner=owner,
                               block=block, extra_arches=extra_arches, force=force, update=update)
     # first check to see if package is:
@@ -2986,16 +2986,6 @@ def lookup_build_target(info, strict=False, create=False):
     return lookup_name('build_target', info, strict, create)
 
 
-def validate_arches_string(arches, strict=True):
-    """run checks against architectures space-separated strings"""
-    if re.match(r'^[a-zA-Z0-9_\- ]+$', arches):
-        return True
-    elif strict:
-        raise koji.GenericError("Architecture can be only [a-zA-Z0-9_-]")
-    else:
-        return False
-
-
 def create_tag(name, parent=None, arches=None, perm=None, locked=False, maven_support=False, maven_include_all=False, extra=None):
     """Create a new tag"""
     context.session.assertPerm('admin')
@@ -3010,8 +3000,7 @@ def _create_tag(name, parent=None, arches=None, perm=None, locked=False, maven_s
         raise koji.GenericError("Tag name %s is too long. Max length is %s characters",
                                 name, max_name_length)
 
-    if arches is not None:
-        validate_arches_string(arches)
+    arches = koji.parse_arches(arches, strict=True, allow_none=True)
 
     if not context.opts.get('EnableMaven') and (maven_support or maven_include_all):
         raise koji.GenericError("Maven support not enabled")
@@ -3196,7 +3185,7 @@ WHERE id = %(tagID)i"""
     # sanitize architecture names (space-separated string)
     arches = kwargs.get('arches')
     if arches and tag['arches'] != arches:
-        validate_arches_string(arches)
+        kwargs['arches'] = koji.parse_arches(arches, strict=True, allow_none=True)
 
     #check for changes
     data = tag.copy()
diff --git a/koji/__init__.py b/koji/__init__.py
index 6e667ce..0d6713c 100644
--- a/koji/__init__.py
+++ b/koji/__init__.py
@@ -1104,6 +1104,37 @@ def canonArch(arch):
     else:
         return arch
 
+def parse_arches(arches, to_list=False, strict=False, allow_none=False):
+    """Normalize user input for a list of arches.
+
+    This method parses a single comma- or space-separated string of arches and
+    returns a space-separated string.
+
+    Raise an error if arches string contain non-allowed characters. In strict
+    version allow only space-separated strings (db input).
+
+    :param str arches: comma- or space-separated string of arches, eg.
+                       "x86_64,ppc64le", or "x86_64 ppc64le"
+    :param bool to_list: return a list of each arch, instead of a single
+                         string. This is False by default.
+    :param bool allow_none: convert None to ""
+    :returns: a space-separated string like "x86_64 ppc64le", or a list like
+              ['x86_64', 'ppc64le'].
+    """
+    if allow_none and arches is None:
+        arches = ''
+    if not strict:
+        arches = arches.replace(',', ' ')
+    if not re.match(r'^[a-zA-Z0-9_\- ]*$', arches):
+        raise GenericError("Architecture can be only [a-zA-Z0-9_-]")
+
+    arches = arches.split()
+    if to_list:
+        return arches
+    else:
+        return ' '.join(arches)
+
+
 class POMHandler(xml.sax.handler.ContentHandler):
     def __init__(self, values, fields):
         xml.sax.handler.ContentHandler.__init__(self)
diff --git a/tests/test_hub/test_validate_arches_string.py b/tests/test_hub/test_validate_arches_string.py
deleted file mode 100644
index 9261421..0000000
--- a/tests/test_hub/test_validate_arches_string.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# coding: utf-8
-from __future__ import absolute_import
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest
-import koji
-import kojihub
-
-class TestValidateArchesString(unittest.TestCase):
-    def test_valid_arches(self):
-        kojihub.validate_arches_string('i386')
-        kojihub.validate_arches_string('i386 x86_64')
-        kojihub.validate_arches_string('i386 x86_64   ')
-
-    def test_invalid_arches(self):
-        with self.assertRaises(koji.GenericError):
-            kojihub.validate_arches_string(u'ěšč')
-
-        with self.assertRaises(koji.GenericError):
-            kojihub.validate_arches_string(u'i386;x86_64')
-
-        with self.assertRaises(koji.GenericError):
-            kojihub.validate_arches_string(u'i386,x86_64')
-
diff --git a/tests/test_lib/test_parse_arches.py b/tests/test_lib/test_parse_arches.py
new file mode 100644
index 0000000..a5aaeac
--- /dev/null
+++ b/tests/test_lib/test_parse_arches.py
@@ -0,0 +1,31 @@
+# coding: utf-8
+from __future__ import absolute_import
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+import koji
+
+class TestParseArchesString(unittest.TestCase):
+    def test_parse_valid_arches(self):
+        r = koji.parse_arches('i386', to_list=True)
+        self.assertEqual(['i386'], r)
+
+        r = koji.parse_arches('i386 x86_64', to_list=True)
+        self.assertEqual(['i386', 'x86_64'], r)
+
+        r = koji.parse_arches('i386 x86_64   ', to_list=True)
+        self.assertEqual(['i386', 'x86_64'], r)
+
+        r = koji.parse_arches('i386,x86_64', to_list=True)
+        self.assertEqual(['i386', 'x86_64'], r)
+
+    def test_parse_invalid_arches(self):
+        with self.assertRaises(koji.GenericError):
+            koji.parse_arches(u'ěšč')
+
+        with self.assertRaises(koji.GenericError):
+            koji.parse_arches(u'i386;x86_64')
+
+        with self.assertRaises(koji.GenericError):
+            koji.parse_arches(u'i386,x86_64', strict=True)

From cab8a1a1f45e2106aec963edc25aec9d074278a1 Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Mar 26 2019 11:52:15 +0000
Subject: [PATCH 3/4] fix test for parse_arches


---

diff --git a/tests/test_hub/test_create_tag.py b/tests/test_hub/test_create_tag.py
index 95daddb..bf79501 100644
--- a/tests/test_hub/test_create_tag.py
+++ b/tests/test_hub/test_create_tag.py
@@ -53,7 +53,7 @@ class TestCreateTag(unittest.TestCase):
         insert = self.inserts[0]
         self.assertEqual(insert.table, 'tag_config')
         values = {
-            'arches': None,
+            'arches': '',
             'create_event': 42,
             'creator_id': 23,
             'locked': False,

From 4ba25ea3e1ac134eb853b66960773f8de7a41438 Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Apr 09 2019 12:34:37 +0000
Subject: [PATCH 4/4] validate archs in addHost


---

diff --git a/hub/kojihub.py b/hub/kojihub.py
index 6477e84..7db5b27 100644
--- a/hub/kojihub.py
+++ b/hub/kojihub.py
@@ -10886,6 +10886,9 @@ class RootExports(object):
     def addHost(self, hostname, arches, krb_principal=None):
         """Add a host to the database"""
         context.session.assertPerm('admin')
+        # validate arches
+        arches = " ".join(arches)
+        arches = koji.parse_arches(arches, strict=True)
         if get_host(hostname):
             raise koji.GenericError('host already exists: %s' % hostname)
         q = """SELECT id FROM channels WHERE name = 'default'"""
@@ -10903,7 +10906,7 @@ class RootExports(object):
         _dml(insert, dslice(locals(), ('hostID', 'userID', 'hostname')))
 
         insert = InsertProcessor('host_config')
-        insert.set(host_id=hostID, arches=" ".join(arches))
+        insert.set(host_id=hostID, arches=arches)
         insert.make_create()
         insert.execute()
 

