From da1313680de707d49b12a9b2347e08eeb4aa7140 Mon Sep 17 00:00:00 2001 From: Matt Jia 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)' % \