From f483d70e61795b7a75b0371851ba75a176ed4eac Mon Sep 17 00:00:00 2001 From: Fabien Boucher Date: Feb 03 2020 14:27:03 +0000 Subject: Add the GET project/webhook/token endpoint This endpoint allows a project's collaborator to access the project webhook token. It is useful in case a collaborator user want to react to project events via the webhook system because it will need to validate the webhook payloads. In particular, third party applications that need to react to multiple projects events wont need to be configured with a long list of token (one for each project) to be able to validate event's payloads. Instead the application will just need to access the projects' endpoint to figure out webhook tokens. --- diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index f09a209..aecde30 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -143,9 +143,12 @@ def get_request_data(): return flask.request.form or flask.request.get_json() or {} -def api_login_required(acls=None): +def api_login_required(acls=None, optional=False): """ Decorator used to indicate that authentication is required for some API endpoint. + + :arg acls: A list of access control + :arg optional: Only check the API token is valid. Skip the ACL validation. """ def decorator(function): @@ -155,7 +158,7 @@ def api_login_required(acls=None): def decorated_function(*args, **kwargs): """ Actually does the job with the arguments provided. """ - response = check_api_acls(acls) + response = check_api_acls(acls, optional) if response: return response @@ -514,6 +517,9 @@ def api(): project.api_modify_project_options ) api_project_block_user_doc = load_doc(project.api_project_block_user) + api_get_project_webhook_token_doc = load_doc( + project.api_get_project_webhook_token + ) issues = [] if pagure_config.get("ENABLE_TICKETS", True): @@ -608,6 +614,7 @@ def api(): api_get_project_options_doc, api_modify_project_options_doc, api_project_block_user_doc, + api_get_project_webhook_token_doc, ], issues=issues, requests=[ diff --git a/pagure/api/project.py b/pagure/api/project.py index 73f1ac6..2a5dd8d 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -2242,6 +2242,62 @@ def api_get_project_connector(repo, username=None, namespace=None): return flask.jsonify({"connector": connector, "status": "ok"}) +@API.route("//webhook/token", methods=["GET"]) +@API.route("///webhook/token", methods=["GET"]) +@API.route("/fork///webhook/token", methods=["GET"]) +@API.route( + "/fork////webhook/token", methods=["GET"] +) +@api_method +@api_login_required(acls=[], optional=True) +def api_get_project_webhook_token(repo, username=None, namespace=None): + """ + Get project webhook token + ------------------------- + Allow project collaborators to retrieve the project webhook token. + + :: + + GET /api/0//webhook/token + GET /api/0///webhook/token + + :: + + GET /api/0/fork///webhook/token + GET /api/0/fork////webhook/token + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "webhook": { + "token": "aaabbbccc", + }, + "status": "ok" + } + + """ + project = _get_repo(repo, username, namespace) + _check_token(project, project_token=False) + + authorized_users = [project.user.username] + # All collaborators are authorized to read the token + for access_type in project.access_users.keys(): + authorized_users.extend( + [user.user for user in project.access_users[access_type]] + ) + if flask.g.fas_user.user not in authorized_users: + raise pagure.exceptions.APIError( + 401, error_code=APIERROR.ENOTHIGHENOUGH + ) + + webhook_token = {"token": project.hook_token} + + return flask.jsonify({"webhook": webhook_token, "status": "ok"}) + + def _check_value(value): """ Convert the provided value into a boolean, an int or leave it as it. """ diff --git a/pagure/utils.py b/pagure/utils.py index 8f3991c..00e02d9 100644 --- a/pagure/utils.py +++ b/pagure/utils.py @@ -68,6 +68,9 @@ def api_authenticated(): def check_api_acls(acls, optional=False): """ Checks if the user provided an API token with its request and if this token allows the user to access the endpoint desired. + + :arg acls: A list of access control + :arg optional: Only check the API token is valid. Skip the ACL validation. """ import pagure.api import pagure.lib.query diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index cc48e75..60b0ed7 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -4327,5 +4327,84 @@ class PagureFlaskApiProjectConnectorTests(tests.Modeltests): self.assertEqual(output.status_code, 401) +class PagureFlaskApiProjectWebhookTokenTests(tests.Modeltests): + """ Tests for the flask API of pagure for getting webhook token of a project + """ + + maxDiff = None + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskApiProjectWebhookTokenTests, self).setUp() + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + # Set a default ACL to avoid get all rights set on + tests.create_tokens_acl(self.session, "aaabbbcccddd", "issue_assign") + + def test_api_get_project_webhook_token_as_owner(self): + """ Test accessing webhook token as project owner. """ + + project = pagure.lib.query._get_project(self.session, "test") + + # Call the endpoint with pingou user token and verify content + headers = {"Authorization": "token aaabbbcccddd"} + output = self.app.get("/api/0/test/webhook/token", headers=headers) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, {"webhook": {"token": project.hook_token}, "status": "ok"} + ) + + def test_api_get_project_webhook_token_as_collaborator(self): + """ Test accessing webhook token as project collaborator. """ + + project = pagure.lib.query._get_project(self.session, "test") + + # Set the foo user as test project collaborator ticket access level + pagure.lib.query.add_user_to_project( + self.session, + project, + new_user="foo", + user="pingou", + access="ticket", + ) + self.session.commit() + + # Create token for foo user with a default ACL + mtoken = pagure.lib.query.add_token_to_user( + self.session, + project=None, + acls=["issue_assign"], + username="foo", + expiration_date=datetime.date.today() + datetime.timedelta(days=1), + ) + + # Call the endpoint with foo user token and verify content + headers = {"Authorization": "token %s" % mtoken.id} + output = self.app.get("/api/0/test/webhook/token", headers=headers) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, {"webhook": {"token": project.hook_token}, "status": "ok"} + ) + + def test_api_get_project_webhook_token_as_not_collaborator(self): + """ Test accessing webhook token as not a project collaborator. """ + + # Create token for foo user with a default ACL + mtoken = pagure.lib.query.add_token_to_user( + self.session, + project=None, + acls=["issue_assign"], + username="foo", + expiration_date=datetime.date.today() + datetime.timedelta(days=1), + ) + + # Call the endpoint with pingou user token and verify content + headers = {"Authorization": "token %s" % mtoken.id} + output = self.app.get("/api/0/test/webhook/token", headers=headers) + self.assertEqual(output.status_code, 401) + + if __name__ == "__main__": unittest.main(verbosity=2)