From 3b70e5568336d1b83e733c9373487ecafa7d8e22 Mon Sep 17 00:00:00 2001
From: Jeremy Stanley <fungi@yuggoth.org>
Date: Fri, 9 May 2014 14:53:14 +0000
Subject: [PATCH] Add a script to normalize Gerrit ACLs

* tools/normalize_acl.py: Script which can perform one or more of a
list of normalizing transformations to an ACL file.

Change-Id: I063ab91b6e786eccaee61c669f0e840c7af6be14
---
 tools/normalize_acl.py | 109 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 109 insertions(+)
 create mode 100755 tools/normalize_acl.py

diff --git a/tools/normalize_acl.py b/tools/normalize_acl.py
new file mode 100755
index 0000000000..d8c9b4297c
--- /dev/null
+++ b/tools/normalize_acl.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+
+# Usage: normalize_acl.py acl.config [transformation [transformation [...]]]
+#
+# Transformations:
+# 0 - dry run (default, print to stdout rather than modifying file in place)
+# 1 - strip/condense whitespace and sort (implied by any other transformation)
+# 2 - get rid of unneeded create on refs/tags
+# 3 - remove any project.stat{e,us} = active since it's a default or a typo
+# 4 - strip default *.owner = group Administrators permissions
+# 5 - sort the exclusiveGroupPermissions group lists
+
+import re
+import sys
+
+aclfile = sys.argv[1]
+
+try:
+    transformations = sys.argv[2:]
+except KeyError:
+    transformations = []
+
+
+def tokens(data):
+    """Human-order comparison
+
+    This handles embedded positive and negative integers, for sorting
+    strings in a more human-friendly order."""
+    data = data.replace('.', ' ').split()
+    for n in range(len(data)):
+        try:
+            data[n] = int(data[n])
+        except ValueError:
+            pass
+    return data
+
+
+acl = {}
+out = ''
+
+if '0' in transformations or not transformations:
+    dry_run = True
+else:
+    dry_run = False
+
+aclfd = open(aclfile)
+for line in aclfd:
+    # condense whitespace to single spaces and get rid of leading/trailing
+    line = re.sub('\s+', ' ', line).strip()
+    # skip empty lines
+    if not line:
+        continue
+    # this is a section heading
+    if line.startswith('['):
+        section = line.strip(' []')
+        # use a list for this because some options can have the same "key"
+        acl[section] = []
+    # key=value lines
+    elif '=' in line:
+        acl[section].append(line)
+    # WTF
+    else:
+        raise Exception('Unrecognized line!')
+aclfd.close()
+
+if '2' in transformations:
+    try:
+        acl['access "refs/tags/*"'] = [
+            x for x in acl['access "refs/tags/*"']
+            if not x.startswith('create = ')]
+    except KeyError:
+        pass
+
+if '3' in transformations:
+    try:
+        acl['project'] = [x for x in acl['project'] if x not in
+                          ('state = active', 'status = active')]
+    except KeyError:
+        pass
+
+if '4' in transformations:
+    for section in acl.keys():
+        acl[section] = [x for x in acl[section] if x !=
+                        'owner = group Administrators']
+
+if '5' in transformations:
+    for section in acl.keys():
+        newsection = []
+        for option in acl[section]:
+            key, value = [x.strip() for x in option.split('=')]
+            if key == 'exclusiveGroupPermissions':
+                newsection.append('%s = %s' % (
+                    key, ' '.join(sorted(value.split()))))
+            else:
+                newsection.append(option)
+        acl[section] = newsection
+
+for section in sorted(acl.keys()):
+    if acl[section]:
+        out += '\n[%s]\n' % section
+        for option in sorted(acl[section], key=tokens):
+            out += '%s\n' % option
+
+if dry_run:
+    print(out[1:-1])
+else:
+    aclfd = open(aclfile, 'w')
+    aclfd.write(out[1:])
+    aclfd.close()