From da1313680de707d49b12a9b2347e08eeb4aa7140 Mon Sep 17 00:00:00 2001
From: Matt Jia <mjia@redhat.com>
Date: Nov 22 2017 04:46:38 +0000
Subject: add proxyuser waiving support


   https://pagure.io/waiverdb/issue/77

---

diff --git a/tests/test_api_v10.py b/tests/test_api_v10.py
index 82ca011..5faf399 100644
--- a/tests/test_api_v10.py
+++ b/tests/test_api_v10.py
@@ -9,7 +9,7 @@ from waiverdb import __version__
 
 
 @patch('waiverdb.auth.get_user', return_value=('foo', {}))
-def test_create_waiver(mocked_get_user, client, session, monkeypatch):
+def test_create_waiver(mocked_get_user, client, session):
     data = {
         'result_id': 123,
         'product_version': 'fool-1',
@@ -39,6 +39,40 @@ def test_create_waiver_with_malformed_data(mocked_get_user, client):
     assert 'invalid literal for int()' in res_data['message']['result_id']
 
 
+@patch('waiverdb.auth.get_user', return_value=('foo', {}))
+def test_non_superuser_cannot_create_waiver_for_other_users(mocked_get_user, client):
+    data = {
+        'result_id': 123,
+        'product_version': 'fool-1',
+        'waived': True,
+        'comment': 'it broke',
+        'proxy_user': 'bar',
+    }
+    r = client.post('/api/v1.0/waivers/', data=json.dumps(data),
+                    content_type='application/json')
+    res_data = json.loads(r.get_data(as_text=True))
+    assert r.status_code == 403
+    assert 'user foo does not have the proxyuser ability' == res_data['message']
+
+
+@patch('waiverdb.auth.get_user', return_value=('bodhi', {}))
+def test_superuser_can_create_waiver_for_other_users(mocked_get_user, client, session):
+    data = {
+        'result_id': 123,
+        'product_version': 'fool-1',
+        'waived': True,
+        'comment': 'it broke',
+        'proxy_user': 'bar',
+    }
+    r = client.post('/api/v1.0/waivers/', data=json.dumps(data),
+                    content_type='application/json')
+    res_data = json.loads(r.get_data(as_text=True))
+    assert r.status_code == 201
+    # a waiver should be created for bar by bodhi
+    assert res_data['username'] == 'bar'
+    assert res_data['proxied_by'] == 'bodhi'
+
+
 def test_get_waiver(client, session):
     # create a new waiver
     waiver = create_waiver(session, result_id=123, username='foo',
@@ -191,6 +225,17 @@ def test_filtering_waivers_by_since(client, session):
     assert len(res_data['data']) == 0
 
 
+def test_filtering_waivers_by_proxied_by(client, session):
+    create_waiver(session, result_id=123, username='foo-1', product_version='foo-1',
+                  proxied_by='bodhi')
+    create_waiver(session, result_id=234, username='foo-2', product_version='foo-1')
+    r = client.get('/api/v1.0/waivers/?proxied_by=bodhi')
+    res_data = json.loads(r.get_data(as_text=True))
+    assert r.status_code == 200
+    assert len(res_data['data']) == 1
+    assert res_data['data'][0]['result_id'] == 123
+
+
 def test_jsonp(client, session):
     waiver = create_waiver(session, result_id=123, username='foo', product_version='foo-1')
     r = client.get('/api/v1.0/waivers/%s?callback=jsonpcallback' % waiver.id)
diff --git a/tests/test_events.py b/tests/test_events.py
index 43c3eba..7193749 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -24,6 +24,35 @@ def test_publish_new_waiver_with_fedmsg(mock_fedmsg, session):
             'id': waiver.id,
             'result_id': 1,
             'username': 'jcline',
+            'proxied_by': None,
+            'product_version': 'something',
+            'waived': True,
+            'comment': 'This is a comment',
+            'timestamp': waiver.timestamp.isoformat(),
+        }
+    )
+
+
+@mock.patch('waiverdb.events.fedmsg')
+def test_publish_new_waiver_with_fedmsg_for_proxy_user(mock_fedmsg, session):
+    waiver = Waiver(
+        result_id=1,
+        username='jcline',
+        product_version='something',
+        waived=True,
+        comment='This is a comment',
+        proxied_by='bodhi'
+    )
+    sesh = session()
+    sesh.add(waiver)
+    sesh.commit()
+    mock_fedmsg.publish.assert_called_once_with(
+        topic='waiver.new',
+        msg={
+            'id': waiver.id,
+            'result_id': 1,
+            'username': 'jcline',
+            'proxied_by': 'bodhi',
             'product_version': 'something',
             'waived': True,
             'comment': 'This is a comment',
diff --git a/tests/utils.py b/tests/utils.py
index 45773fc..77d76e3 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,8 +4,8 @@ from waiverdb.models import Waiver
 
 
 def create_waiver(session, result_id, username, product_version, waived=True,
-                  comment=None):
-    waiver = Waiver(result_id, username, product_version, waived, comment)
+                  comment=None, proxied_by=None):
+    waiver = Waiver(result_id, username, product_version, waived, comment, proxied_by)
     session.add(waiver)
     session.flush()
     return waiver
diff --git a/waiverdb/api_v1.py b/waiverdb/api_v1.py
index 14d1eb2..d16389b 100644
--- a/waiverdb/api_v1.py
+++ b/waiverdb/api_v1.py
@@ -2,7 +2,7 @@
 
 from flask import Blueprint, request, current_app
 from flask_restful import Resource, Api, reqparse, marshal_with, marshal
-from werkzeug.exceptions import BadRequest, NotFound, UnsupportedMediaType
+from werkzeug.exceptions import BadRequest, NotFound, UnsupportedMediaType, Forbidden
 from sqlalchemy.sql.expression import func
 
 from waiverdb import __version__
@@ -23,6 +23,7 @@ RP['create_waiver'].add_argument('result_id', type=int, required=True, location=
 RP['create_waiver'].add_argument('waived', type=bool, required=True, location='json')
 RP['create_waiver'].add_argument('product_version', type=str, required=True, location='json')
 RP['create_waiver'].add_argument('comment', type=str, default=None, location='json')
+RP['create_waiver'].add_argument('proxy_user', type=str, default=None, location='json')
 
 RP['get_waivers'] = reqparse.RequestParser()
 RP['get_waivers'].add_argument('result_id', location='args')
@@ -34,6 +35,7 @@ RP['get_waivers'].add_argument('include_obsolete', type=bool, default=False, loc
 RP['get_waivers'].add_argument('since', location='args')
 RP['get_waivers'].add_argument('page', default=1, type=int, location='args')
 RP['get_waivers'].add_argument('limit', default=10, type=int, location='args')
+RP['get_waivers'].add_argument('proxied_by', location='args')
 
 
 class WaiversResource(Resource):
@@ -76,6 +78,8 @@ class WaiversResource(Resource):
             more result IDs separated by commas.
         :query string product_version: Filter the waivers by product version.
         :query string username: Filter the waivers by username.
+        :query string proxied_by: Filter the waivers by the users who are
+            allowed to create waivers on behalf of other users.
         :query string since: An ISO 8601 formatted datetime (e.g. 2017-03-16T13:40:05+00:00)
             to filter results by. Optionally provide a second ISO 8601 datetime separated
             by a comma to retrieve a range (e.g. 2017-03-16T13:40:05+00:00,
@@ -93,6 +97,8 @@ class WaiversResource(Resource):
             query = query.filter(Waiver.product_version == args['product_version'])
         if args['username']:
             query = query.filter(Waiver.username == args['username'])
+        if args['proxied_by']:
+            query = query.filter(Waiver.proxied_by == args['proxied_by'])
         if args['since']:
             try:
                 since_start, since_end = reqparse_since(args['since'])
@@ -152,19 +158,27 @@ class WaiversResource(Resource):
                "result_id": 1,
                "timestamp": "2017-03-16T17:42:04.209638",
                "username": "jcline",
-               "waived": false
+               "waived": false,
+               "proxied_by": null
            }
 
         :json int result_id: The result ID for the waiver.
         :json boolean waived: Whether or not the result is waived.
         :json string product_version: The product version string.
         :json string comment: A comment explaining the waiver.
+        :json string proxy_user: Username on whose behalf the caller is proxying.
         :statuscode 201: The waiver was successfully created.
         """
         user, headers = waiverdb.auth.get_user(request)
         args = RP['create_waiver'].parse_args()
+        proxied_by = None
+        if args.get('proxy_user'):
+            if user not in current_app.config['SUPERUSERS']:
+                raise Forbidden('user %s does not have the proxyuser ability' % user)
+            proxied_by = user
+            user = args['proxy_user']
         waiver = Waiver(args['result_id'], user, args['product_version'], args['waived'],
-                        args['comment'])
+                        args['comment'], proxied_by)
         db.session.add(waiver)
         db.session.commit()
         return waiver, 201, headers
@@ -233,7 +247,8 @@ class GetWaiversByResultIDs(Resource):
                         "result_id": 2,
                         "timestamp": "2017-09-21T04:55:53.343368",
                         "username": "dummy",
-                        "waived": true
+                        "waived": true,
+                        "proxied_by": null
                     },
                     {
                         "comment": "It's dead!",
@@ -242,7 +257,8 @@ class GetWaiversByResultIDs(Resource):
                         "result_id": 1,
                         "timestamp": "2017-09-21T04:55:51.936658",
                         "username": "dummy",
-                        "waived": true
+                        "waived": true,
+                        "proxied_by": null
                     }
                 ]
             }
@@ -250,6 +266,8 @@ class GetWaiversByResultIDs(Resource):
         :jsonparam array result_ids: Filter the waivers by a list of result IDs.
         :jsonparam string product_version: Filter the waivers by product version.
         :jsonparam string username: Filter the waivers by username.
+        :jsonparam string proxied_by: Filter the waivers by the users who are
+            allowed to create waivers on behalf of other users.
         :jsonparam string since: An ISO 8601 formatted datetime (e.g. 2017-03-16T13:40:05+00:00)
             to filter results by. Optionally provide a second ISO 8601 datetime separated
             by a comma to retrieve a range (e.g. 2017-03-16T13:40:05+00:00,
@@ -268,6 +286,8 @@ class GetWaiversByResultIDs(Resource):
             query = query.filter(Waiver.product_version == data['product_version'])
         if 'username' in data:
             query = query.filter(Waiver.username == data['username'])
+        if 'proxied_by' in data:
+            query = query.filter(Waiver.proxied_by == data['proxied_by'])
         if 'since' in data:
             try:
                 since_start, since_end = reqparse_since(data['since'])
diff --git a/waiverdb/config.py b/waiverdb/config.py
index 6c46c46..910f0e5 100644
--- a/waiverdb/config.py
+++ b/waiverdb/config.py
@@ -30,6 +30,8 @@ class Config(object):
     # Specify fedmsg or stomp for publishing messages
     MESSAGE_PUBLISHER = 'fedmsg'
     SQLALCHEMY_TRACK_MODIFICATIONS = True
+    # A list of users are allowed to create waivers on behalf of other users.
+    SUPERUSERS = []
 
 
 class ProductionConfig(Config):
@@ -61,3 +63,4 @@ class TestingConfig(Config):
     )
     OIDC_REQUIRED_SCOPE = 'waiverdb_scope'
     OIDC_RESOURCE_SERVER_ONLY = True
+    SUPERUSERS = ['bodhi']
diff --git a/waiverdb/events.py b/waiverdb/events.py
index 6cbd07c..372ab48 100644
--- a/waiverdb/events.py
+++ b/waiverdb/events.py
@@ -49,6 +49,7 @@ def publish_new_waiver(session):
             "timestamp": "2017-03-16T17:42:04.209638",
             "product_version": "Satellite 6.3",
             "result_id": 1,
+            "proxied_by": null,
             "id": 15
           }
         }
diff --git a/waiverdb/fields.py b/waiverdb/fields.py
index 1bd1307..b724b12 100644
--- a/waiverdb/fields.py
+++ b/waiverdb/fields.py
@@ -6,6 +6,7 @@ waiver_fields = {
     'id': fields.Integer,
     'result_id': fields.Integer,
     'username': fields.String,
+    'proxied_by': fields.String,
     'product_version': fields.String,
     'waived': fields.Boolean,
     'comment': fields.String,
diff --git a/waiverdb/migrations/versions/0a74cdab732a_add_proxyuser_waiving_support.py b/waiverdb/migrations/versions/0a74cdab732a_add_proxyuser_waiving_support.py
new file mode 100644
index 0000000..aba62c1
--- /dev/null
+++ b/waiverdb/migrations/versions/0a74cdab732a_add_proxyuser_waiving_support.py
@@ -0,0 +1,26 @@
+"""add proxyuser waiving support
+
+Revision ID: 0a74cdab732a
+Revises: 0a27a8ad723a
+Create Date: 2017-11-22 13:31:50.213681
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '0a74cdab732a'
+down_revision = '0a27a8ad723a'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('waiver', sa.Column('proxied_by', sa.String(length=255), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('waiver', 'proxied_by')
+    # ### end Alembic commands ###
diff --git a/waiverdb/models/waivers.py b/waiverdb/models/waivers.py
index 6760ca0..317badf 100644
--- a/waiverdb/models/waivers.py
+++ b/waiverdb/models/waivers.py
@@ -8,17 +8,20 @@ class Waiver(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     result_id = db.Column(db.Integer, nullable=False)
     username = db.Column(db.String(255), nullable=False)
+    proxied_by = db.Column(db.String(255))
     product_version = db.Column(db.String(200), nullable=False)
     waived = db.Column(db.Boolean, nullable=False, default=False)
     comment = db.Column(db.Text)
     timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow)
 
-    def __init__(self, result_id, username, product_version, waived=False, comment=None):
+    def __init__(self, result_id, username, product_version, waived=False, comment=None,
+                 proxied_by=None):
         self.result_id = result_id
         self.username = username
         self.product_version = product_version
         self.waived = waived
         self.comment = comment
+        self.proxied_by = proxied_by
 
     def __repr__(self):
         return '%s(result_id=%r, username=%r, product_version=%r, waived=%r)' % \

