From dfb30c467d6df17687d0c7a7ab3d943b1ca10636 Mon Sep 17 00:00:00 2001 From: Fabien Boucher Date: Feb 01 2019 11:12:15 +0000 Subject: [PATCH 1/3] Add project connector api endpoint This endpoint aims to provide a get access to connector tokens that are: - project hook token - project's user (caller) API token I call them connector data as those tokens are the mandatory data an application should have to verify project events received via the Web hooks but also to perform actions via the API to a given project. The endpoint is only available to project's owner and admin users with a 'modify_project' ACL token. Thanks to this endpoint, a user (or an application) added as admin on one or more projects, and via its user token, would be able to read connector data on those projects and then received events and perform API call on those same projects. --- diff --git a/pagure/api/project.py b/pagure/api/project.py index eb74109..4e706d1 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -1963,6 +1963,79 @@ def api_get_project_options(repo, username=None, namespace=None): return flask.jsonify({"settings": project.settings, "status": "ok"}) +@API.route("//connector", methods=["GET"]) +@API.route("///connector", methods=["GET"]) +@API.route("/fork///connector", methods=["GET"]) +@API.route("/fork////connector", methods=["GET"]) +@api_login_required(acls=["modify_project"]) +@api_method +def api_get_project_connector(repo, username=None, namespace=None): + """ + Get project connector + --------------------- + Allow project owner and admins to retrieve connector tokens. + Connector tokens are the API tokens and the Web Hook token + of the project. Connector tokens make possible for an external + application to listen and verify project notifications and act + on project via the REST API. + + :: + + GET /api/0//connector + GET /api/0///connector + + :: + + GET /api/0/fork///connector + GET /api/0/fork////connector + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "connector": { + "hook_token": "aaabbbccc", + "api_token": [ + {'name': 'foo token', 'id': "abcdefoo"} + {'name': 'bar token', 'id': "abcdebar"} + ] + }, + "status": "ok" + } + + """ + project = get_authorized_api_project( + flask.g.session, repo, namespace=namespace + ) + if not project: + raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) + + if flask.g.token.project and project != flask.g.token.project: + raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) + + authorized_users = [project.user.username] + authorized_users.extend( + [user.user for user in project.access_users['admin']]) + if flask.g.fas_user.user not in authorized_users: + raise pagure.exceptions.APIError( + 401, error_code=APIERROR.ENOTHIGHENOUGH) + + user_obj = pagure.lib.query.search_user( + flask.g.session, username=flask.g.fas_user.user) + user_project_tokens = [ + token for token in user_obj.tokens if token.project_id == project.id] + + connector = { + 'hook_token': project.hook_token, + 'api_tokens': [ + {'name': t.description, 'id': t.id} for t in user_project_tokens] + } + + return flask.jsonify({"connector": connector, "status": "ok"}) + + def _check_value(value): """ Convert the provided value into a boolean, an int or leave it as it. """ diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index d0a906c..486d6e9 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -4097,5 +4097,158 @@ class PagureFlaskApiProjectOptionsTests(tests.Modeltests): self.assertEqual(after, before) +class PagureFlaskApiProjectConnectorTests(tests.Modeltests): + """ Tests for the flask API of pagure for getting connector of a project + """ + + maxDiff = None + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskApiProjectConnectorTests, self).setUp() + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + + + def test_api_get_project_connector_as_owner(self): + """ Test accessing api_get_project_connector as project owner. """ + + project = pagure.lib.query._get_project(self.session, 'test') + + # Create witness project Token for pingou user + pagure.lib.query.add_token_to_user( + self.session, + project=project, + acls=['pull_request_merge'], + username='pingou') + ctokens = pagure.lib.query.search_token( + self.session, ['pull_request_merge'], user='pingou') + + # Call the connector with pingou user token and verify content + headers = {'Authorization': 'token aaabbbcccddd'} + output = self.app.get('/api/0/test/connector', headers=headers) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, + {"connector": { + "hook_token": project.hook_token, + "api_tokens": [ + {'name': t.description, 'id': t.id} for t in ctokens] + }, + "status": "ok" + } + ) + + def test_api_get_project_connector_as_admin(self): + """ Test accessing api_get_project_connector as project admin """ + + project = pagure.lib.query._get_project(self.session, 'test') + + # Set the foo user as test project admin + pagure.lib.query.add_user_to_project( + self.session, project, + new_user='foo', + user='pingou', + access='admin' + ) + self.session.commit() + + # Create modify_project token for foo user + pagure.lib.query.add_token_to_user( + self.session, + project=None, + acls=['modify_project'], + username='foo') + mtoken = pagure.lib.query.search_token( + self.session, ['modify_project'], user='foo')[0] + + # Create witness project Token for foo user + pagure.lib.query.add_token_to_user( + self.session, + project=project, + acls=['pull_request_merge'], + username='foo') + ctokens = pagure.lib.query.search_token( + self.session, ['pull_request_merge'], user='foo') + + # Call the connector with foo user token and verify content + headers = {'Authorization': 'token %s' % mtoken.id} + output = self.app.get('/api/0/test/connector', headers=headers) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, + {"connector": { + "hook_token": project.hook_token, + "api_tokens": [ + {'name': t.description, 'id': t.id} for t in ctokens] + }, + "status": "ok" + } + ) + + def test_api_get_project_connector_as_unauthorized(self): + """ Test accessing api_get_project_connector as project admin + but with unauthorized token ACL + """ + + project = pagure.lib.query._get_project(self.session, 'test') + + # Set the foo user as test project admin + pagure.lib.query.add_user_to_project( + self.session, project, + new_user='foo', + user='pingou', + access='admin' + ) + self.session.commit() + + # Create modify_project token for foo user + pagure.lib.query.add_token_to_user( + self.session, + project=None, + acls=['create_project'], + username='foo') + mtoken = pagure.lib.query.search_token( + self.session, ['create_project'], user='foo')[0] + + # Call the connector with foo user token and verify unauthorized + headers = {'Authorization': 'token %s' % mtoken.id} + output = self.app.get('/api/0/test/connector', headers=headers) + self.assertEqual(output.status_code, 401) + + def test_api_get_project_connector_as_unauthorized_2(self): + """ Test accessing api_get_project_connector as project + but with unauthorized token ACL + """ + + project = pagure.lib.query._get_project(self.session, 'test') + + # Set the foo user as test project admin + pagure.lib.query.add_user_to_project( + self.session, project, + new_user='foo', + user='pingou', + access='commit' + ) + self.session.commit() + + # Create modify_project token for foo user + pagure.lib.query.add_token_to_user( + self.session, + project=None, + acls=['modify_project'], + username='foo') + mtoken = pagure.lib.query.search_token( + self.session, ['modify_project'], user='foo')[0] + + # Call the connector with foo user token and verify unauthorized + headers = {'Authorization': 'token %s' % mtoken.id} + output = self.app.get('/api/0/test/connector', headers=headers) + self.assertEqual(output.status_code, 401) + if __name__ == '__main__': unittest.main(verbosity=2) From 346ee5375990c77d65dd14596ecf603d5cea8287 Mon Sep 17 00:00:00 2001 From: Fabien Boucher Date: Feb 01 2019 11:12:15 +0000 Subject: [PATCH 2/3] Api: project connector endpoint: complete returned data Add the expriration status of API tokens to the returned data set. --- diff --git a/pagure/api/project.py b/pagure/api/project.py index 4e706d1..02d7672 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -2030,7 +2030,11 @@ def api_get_project_connector(repo, username=None, namespace=None): connector = { 'hook_token': project.hook_token, 'api_tokens': [ - {'name': t.description, 'id': t.id} for t in user_project_tokens] + {'description': t.description, + 'id': t.id, + 'expired': t.expired + } for t in user_project_tokens + ] } return flask.jsonify({"connector": connector, "status": "ok"}) diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index 486d6e9..eeae27a 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -4136,7 +4136,9 @@ class PagureFlaskApiProjectConnectorTests(tests.Modeltests): {"connector": { "hook_token": project.hook_token, "api_tokens": [ - {'name': t.description, 'id': t.id} for t in ctokens] + {'description': t.description, + 'id': t.id, + 'expired': False} for t in ctokens] }, "status": "ok" } @@ -4184,7 +4186,9 @@ class PagureFlaskApiProjectConnectorTests(tests.Modeltests): {"connector": { "hook_token": project.hook_token, "api_tokens": [ - {'name': t.description, 'id': t.id} for t in ctokens] + {'description': t.description, + 'id': t.id, + 'expired': False} for t in ctokens] }, "status": "ok" } From 0bccc88cdcdc98219995f8827f05e3ea28544dc9 Mon Sep 17 00:00:00 2001 From: Fabien Boucher Date: Feb 01 2019 11:12:15 +0000 Subject: [PATCH 3/3] Fix docstring of the connector api endpoint --- diff --git a/pagure/api/project.py b/pagure/api/project.py index 02d7672..2ba4f91 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -1973,7 +1973,7 @@ def api_get_project_connector(repo, username=None, namespace=None): """ Get project connector --------------------- - Allow project owner and admins to retrieve connector tokens. + Allow project owners and admins to retrieve their own connector tokens. Connector tokens are the API tokens and the Web Hook token of the project. Connector tokens make possible for an external application to listen and verify project notifications and act @@ -1998,8 +1998,12 @@ def api_get_project_connector(repo, username=None, namespace=None): "connector": { "hook_token": "aaabbbccc", "api_token": [ - {'name': 'foo token', 'id': "abcdefoo"} - {'name': 'bar token', 'id': "abcdebar"} + {'name': 'foo token', + 'id': "abcdefoo", + 'expired': True} + {'name': 'bar token', + 'id': "abcdebar", + 'expired': False} ] }, "status": "ok" diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index eeae27a..22ea91f 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -4125,6 +4125,7 @@ class PagureFlaskApiProjectConnectorTests(tests.Modeltests): username='pingou') ctokens = pagure.lib.query.search_token( self.session, ['pull_request_merge'], user='pingou') + self.assertEqual(len(ctokens), 1) # Call the connector with pingou user token and verify content headers = {'Authorization': 'token aaabbbcccddd'} @@ -4175,6 +4176,7 @@ class PagureFlaskApiProjectConnectorTests(tests.Modeltests): username='foo') ctokens = pagure.lib.query.search_token( self.session, ['pull_request_merge'], user='foo') + self.assertEqual(len(ctokens), 1) # Call the connector with foo user token and verify content headers = {'Authorization': 'token %s' % mtoken.id}