Repository: fedora-infra/bodhi

Issue:
Add bodhi-sar
I created a script for Fedora that gathers all the data Bodhi knows about a user account and prints it to stdout in JSON format:

https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/bodhi2/backend/files/bodhi_sar.py

It would be best to add this script to Bodhi upstream so that we can test and maintain it together with Bodhi, and so that other Bodhi deployments can respond to GDPR requests.


Discussion:
We could also add an option to pretty-print the JSON.

Resolving patch:
diff --git a/bodhi/server/scripts/sar.py b/bodhi/server/scripts/sar.py
new file mode 100644
--- /dev/null
+++ b/bodhi/server/scripts/sar.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2018 Red Hat, Inc. and Sebastian Wojciechowski
+#
+# This file is part of Bodhi.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+"""This script will print out SAR data for a FAS account."""
+
+import json
+import sys
+
+import click
+import sqlalchemy
+
+from bodhi.server import config, initialize_db, models
+
+
+def print_human_readable_format(sar_data):
+    """
+    Print user data in human readable format.
+
+    Args:
+        sar_data (dict): User data to be printed.
+    """
+    header_start = "==========>"
+    header_stop = "<=========="
+    chapter_start = "---->"
+    chapter_stop = "<----"
+
+    for user in sar_data:
+        click.echo("{} User account data for: {} {}\n".format(
+            header_start, sar_data[user]['name'], header_stop
+        ))
+        click.echo("email: {}".format(sar_data[user]['email']))
+        click.echo("groups: {}".format(sar_data[user]['groups']))
+        click.echo("popups: {}".format(sar_data[user]['show_popups']))
+
+        click.echo("\n{} Comments: {}".format(chapter_start, chapter_stop))
+        for idx, comment in enumerate(sar_data[user]['comments'], 1):
+            click.echo("\nComment no {}:".format(idx))
+            for item in sorted(comment):
+                click.echo("{}: {}".format(item, comment[item]))
+
+        click.echo("\n{} Updates: {}".format(chapter_start, chapter_stop))
+        for idx, update in enumerate(sar_data[user]['updates'], 1):
+            click.echo("\nUpdate no {}:".format(idx))
+            for item in sorted(update):
+                click.echo("{}: {}".format(item, update[item]))
+
+
+@click.command()
+@click.option('--username', envvar='SAR_USERNAME', required=True,
+              help='The username that sar data should be gathered.')
+@click.option('--human-readable', default=False, is_flag=True,
+              help='Print user data in human readable format.'
+              ' The script defaults to JSON output.'
+              )
+@click.version_option(message='%(version)s')
+def get_user_data(username, human_readable):
+    """Get username SAR data."""
+    initialize_db(config.config)
+
+    sar_data = {}
+
+    try:
+        user = models.User.query.filter_by(name=username).one()
+    except sqlalchemy.orm.exc.NoResultFound:
+        if human_readable:
+            click.echo("User not found.")
+        sys.exit(0)
+
+    sar_data[user.name] = {}
+    sar_data[user.name]['comments'] = [
+        {'karma': c.karma, 'karma_critpath': c.karma_critpath, 'text': c.text,
+         'anonymous': c.anonymous, 'timestamp': c.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
+         'update_alias': c.update.alias, 'username': c.user.name}
+        for c in user.comments]
+    sar_data[user.name]['email'] = user.email
+    sar_data[user.name]['groups'] = [g.name for g in user.groups]
+    sar_data[user.name]['name'] = user.name
+    sar_data[user.name]['show_popups'] = user.show_popups
+    sar_data[user.name]['updates'] = [
+        {'autokarma': u.autokarma, 'stable_karma': u.stable_karma,
+         'unstable_karma': u.unstable_karma, 'requirements': u.requirements,
+         'require_bugs': u.require_bugs, 'require_testcases': u.require_testcases,
+         'notes': u.notes, 'type': str(u.type), 'severity': str(u.severity),
+         'suggest': str(u.suggest), 'close_bugs': u.close_bugs, 'alias': u.alias,
+         'builds': [b.nvr for b in u.builds], 'release_name': u.release.name,
+         'bugs': [b.bug_id for b in u.bugs], 'user': u.user.name,
+         'date_submitted': u.date_submitted.strftime('%Y-%m-%d %H:%M:%S')}
+        for u in user.updates]
+
+    if human_readable:
+        print_human_readable_format(sar_data)
+    else:
+        click.echo(json.dumps(sar_data, sort_keys=True))
+
+
+if __name__ == '__main__':
+    get_user_data()
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -163,6 +163,7 @@ def get_requirements(requirements_file='requirements.txt'):
     bodhi-manage-releases = bodhi.server.scripts.manage_releases:main
     bodhi-check-policies = bodhi.server.scripts.check_policies:check
     bodhi-skopeo-lite = bodhi.server.scripts.skopeo_lite:main
+    bodhi-sar = bodhi.server.scripts.sar:get_user_data
     [moksha.consumer]
     masher = bodhi.server.consumers.masher:Masher
     updates = bodhi.server.consumers.updates:UpdatesHandler


Test patch:
diff --git a/bodhi/tests/server/scripts/test_sar.py b/bodhi/tests/server/scripts/test_sar.py
new file mode 100644
--- /dev/null
+++ b/bodhi/tests/server/scripts/test_sar.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# Copyright © 2018 Sebastian Wojciechowski
+#
+# This file is part of Bodhi.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+"""
+This module contains tests for the bodhi.server.scripts.sar module.
+"""
+import os
+from datetime import datetime
+
+from click import testing
+import mock
+
+from bodhi.server import models
+from bodhi.server.scripts import sar
+from bodhi.tests.server.base import BaseTestCase
+
+
+EXPECTED_USER_DATA_OUTPUT = """\
+==========> User account data for: guest <==========
+
+email: None
+groups: ['packager']
+popups: True
+
+----> Comments: <----
+
+Comment no 1:
+anonymous: False
+karma: 1
+karma_critpath: 0
+text: wow. amaze.
+timestamp: 1984-11-02 00:00:00
+update_alias: FEDORA-2018-a3bbe1a8f2
+username: guest
+
+----> Updates: <----
+
+Update no 1:
+alias: FEDORA-2018-a3bbe1a8f2
+autokarma: True
+bugs: [12345]
+builds: ['bodhi-2.0-1.fc17']
+close_bugs: True
+date_submitted: 1984-11-02 00:00:00
+notes: Useful details!
+release_name: F17
+require_bugs: False
+require_testcases: False
+requirements: rpmlint
+severity: medium
+stable_karma: 3
+suggest: unspecified
+type: bugfix
+unstable_karma: -3
+user: guest
+"""
+
+EXPECTED_JSON_OUTPUT = (
+    '{"guest": {"comments": [{"anonymous": false, "karma": 1, "karma_critpath": 0, "text":'
+    ' "wow. amaze.", "timestamp": "1984-11-02 00:00:00", "update_alias": '
+    '"FEDORA-2018-a3bbe1a8f2", "username": "guest"}], "email": null, "groups": '
+    '["packager"], "name": "guest", "show_popups": true, "updates": [{"alias": '
+    '"FEDORA-2018-a3bbe1a8f2", "autokarma": true, "bugs": [12345], "builds": '
+    '["bodhi-2.0-1.fc17"], "close_bugs": true, "date_submitted": "1984-11-02 00:00:00", '
+    '"notes": "Useful details!", "release_name": "F17", "require_bugs": false, '
+    '"require_testcases": false, "requirements": "rpmlint", "severity": "medium", '
+    '"stable_karma": 3, "suggest": "unspecified", "type": "bugfix", '
+    '"unstable_karma": -3, "user": "guest"}]}}\n'
+)
+
+
+class TestSar(BaseTestCase):
+    """This class contains tests for the get_user_data() function."""
+
+    def test_invalid_user(self):
+        """Ensure nothing is printed when user is not found and human readable is off."""
+        runner = testing.CliRunner()
+        r = runner.invoke(sar.get_user_data, ["--username=" + "invalid_user"])
+
+        self.assertEqual(r.exit_code, 0)
+        self.assertEqual(r.output, "")
+
+    def test_invalid_user_human_readable(self):
+        """Ensure proper info is printed when user is not found and human readable is on."""
+        runner = testing.CliRunner()
+        r = runner.invoke(sar.get_user_data, ["--username=" + "invalid_user", "--human-readable"])
+
+        self.assertEqual(r.exit_code, 0)
+        self.assertEqual(r.output, u"User not found.\n")
+
+    def test_valid_user(self):
+        """Ensure json with user data is printed when human readable is off."""
+        now = datetime.utcnow()
+        now_str = str(now.strftime("%Y-%m-%d %H:%M:%S"))
+        comment = self.db.query(models.Comment).all()[0]
+        comment.timestamp = now
+        self.db.commit()
+        expected_output = EXPECTED_JSON_OUTPUT.replace("1984-11-02 00:00:00", now_str, 1)
+
+        runner = testing.CliRunner()
+        r = runner.invoke(sar.get_user_data, ["--username=" + "guest"])
+
+        self.assertEqual(r.exit_code, 0)
+        self.assertEqual(r.output, expected_output)
+
+    @mock.patch.dict(os.environ, {"SAR_USERNAME": "guest"})
+    def test_valid_user_envvar(self):
+        """Ensure json with user data is printed when human readable is off."""
+        now = datetime.utcnow()
+        now_str = str(now.strftime("%Y-%m-%d %H:%M:%S"))
+        comment = self.db.query(models.Comment).all()[0]
+        comment.timestamp = now
+        self.db.commit()
+        expected_output = EXPECTED_JSON_OUTPUT.replace("1984-11-02 00:00:00", now_str, 1)
+
+        runner = testing.CliRunner()
+        r = runner.invoke(sar.get_user_data)
+
+        self.assertEqual(r.exit_code, 0)
+        self.assertEqual(r.output, expected_output)
+
+    def test_valid_user_human_readable(self):
+        """Ensure user data are printed when human readable is on."""
+        now = datetime.utcnow()
+        now_str = str(now.strftime("%Y-%m-%d %H:%M:%S"))
+        comment = self.db.query(models.Comment).all()[0]
+        comment.timestamp = now
+        self.db.commit()
+        expected_output = EXPECTED_USER_DATA_OUTPUT.replace("1984-11-02 00:00:00", now_str, 1)
+
+        runner = testing.CliRunner()
+        r = runner.invoke(sar.get_user_data, ["--username=" + "guest", "--human-readable"])
+
+        self.assertEqual(r.exit_code, 0)
+        # Since py2 and py3 display list with strings in different way,
+        # we have to change adjust script output a little bit
+        self.assertEqual(r.output.replace("u'", "'"), expected_output)