From 0bf7de9bfcaa7885bca3aa5fa34a1f752d920c01 Mon Sep 17 00:00:00 2001 From: Fabien Boucher Date: Feb 11 2019 10:11:57 +0000 Subject: Add project createapitoken endpoint A project owner or admin (with the 'modify_project') acl can ask the creation of a project user token. The token description and acl can be passed to the endpoint. The endpoint return the token id and the description. --- diff --git a/pagure/api/project.py b/pagure/api/project.py index 2f3b159..83ab5b3 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -2133,3 +2133,109 @@ def api_modify_project_options(repo, username=None, namespace=None): raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) return flask.jsonify({"message": message, "status": "ok"}) + +@API.route("//createapitoken", methods=["POST"]) +@API.route("///createapitoken", methods=["POST"]) +@API.route("/fork///createapitoken", methods=["POST"]) +@API.route( + "/fork////createapitoken", methods=["POST"] +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_project_create_api_token(repo, namespace=None, username=None): + """ + Create API project Token + ------------------------ + Create a project token API for the caller user + + This is restricted to project admins. + + :: + + POST /api/0//createapitoken + POST /api/0///createapitoken + + :: + + POST /api/0/fork///createapitoken + POST /api/0/fork////createapitoken + + + Input + ^^^^^ + + +------------------+---------+---------------+---------------------------+ + | Key | Type | Optionality | Description | + +==================+=========+===============+===========================+ + | ``desc`` | String | Mandatory | A string to specify the | + | | | | description of the token | + | | | | | + +------------------+---------+---------------+---------------------------+ + | ``acl`` | String | Mandatory | The ACL as a comma | + | | | | string | + | | | | | + +------------------+---------+---------------+---------------------------+ + + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "token": { + "description": "My foo token", + "id": "aaabbbcccfootoken", + }, + } + + """ + output = {} + 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) + + + form = flask.request.form + valid_form = True + description = form.get('description') + acl = form.get('acl') + if not isinstance(description, str) or not isinstance(acl, str): + valid_form = False + acl_list = acl.split(',') + for ac in acl_list: + if ac not in pagure_config.get("ACLS", []): + valid_form = False + break + if not valid_form: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EINVALIDREQ) + + pagure.lib.query.add_token_to_user( + flask.g.session, project, acl.split(','), flask.g.fas_user.user, + description) + token_id = pagure.lib.query.search_token( + flask.g.session, None, user=flask.g.fas_user.user, + description=description)[0].id + output = { + 'token': { + 'description': description, + 'id': token_id + } + } + + jsonout = flask.jsonify(output) + return jsonout diff --git a/pagure/lib/query.py b/pagure/lib/query.py index af39174..6749016 100644 --- a/pagure/lib/query.py +++ b/pagure/lib/query.py @@ -5205,8 +5205,8 @@ def get_obj_access(session, project_obj, obj): def search_token( - session, acls, user=None, token=None, active=False, expired=False -): + session, acls, user=None, token=None, active=False, expired=False, + description=None): """ Searches the API tokens corresponding to the criterias specified. :arg session: the session to use to connect to the database. @@ -5214,6 +5214,7 @@ def search_token( :arg user: restrict the API tokens to this given user :arg token: restrict the API tokens to this specified token (if it exists) + :arg description: restrict the API tokens to this given description """ query = ( session.query(model.Token) @@ -5232,6 +5233,9 @@ def search_token( model.User.user == user ) + if description: + query = query.filter(model.Token.description == description) + if active: query = query.filter( model.Token.expiration > datetime.datetime.utcnow() diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index 8502d68..39d3022 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -4096,6 +4096,179 @@ class PagureFlaskApiProjectOptionsTests(tests.Modeltests): before["settings"]["issues_default_to_private"] = True self.assertEqual(after, before) +class PagureFlaskApiProjectCreateAPITokenTests(tests.Modeltests): + """ Tests for the flask API of pagure for creating user project API token + """ + + maxDiff = None + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskApiProjectCreateAPITokenTests, 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_createapitoken_as_owner(self): + """ Test accessing api_project_createapitoken as owner. """ + + headers = {'Authorization': 'token aaabbbcccddd'} + project = pagure.lib.query._get_project(self.session, 'test') + tdescription = 'my new token' + + # Call the api with pingou user token and verify content + data = { + 'description': tdescription, + 'acl': 'pull_request_merge,pull_request_comment' + } + output = self.app.post('/api/0/test/createapitoken', + headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + tid = pagure.lib.query.search_token( + self.session, None, description=tdescription)[0].id + self.assertEqual( + data, + {"token": { + "description": tdescription, + "id": tid + } + } + ) + # Create a second token but with faulty acl + # Call the api with pingou user token and error code + data = { + 'description': tdescription, + 'acl': 'foo,bar' + } + output = self.app.post('/api/0/test/createapitoken', + headers=headers, data=data) + self.assertEqual(output.status_code, 400) + + def test_api_createapitoken_as_admin(self): + """ Test accessing api_project_createapitoken as 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] + + # Call the connector with foo user token and verify content + headers = {'Authorization': 'token %s' % mtoken.id} + tdescription = 'my new token' + + # Call the api with pingou user token and verify content + data = { + 'description': tdescription, + 'acl': 'pull_request_merge,pull_request_comment' + } + output = self.app.post('/api/0/test/createapitoken', + headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + tid = pagure.lib.query.search_token( + self.session, None, user='foo', description=tdescription)[0].id + self.assertEqual( + data, + {"token": { + "description": tdescription, + "id": tid + } + } + ) + + def test_api_createapitoken_as_unauthorized(self): + """ Test accessing api_project_createapitoken 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_branch'], + username='foo') + mtoken = pagure.lib.query.search_token( + self.session, ['create_branch'], user='foo')[0] + + # Call the connector with foo user token and verify content + headers = {'Authorization': 'token %s' % mtoken.id} + tdescription = 'my new token' + + # Call the api with pingou user token and verify content + data = { + 'description': tdescription, + 'acl': 'pull_request_merge,pull_request_comment' + } + output = self.app.post('/api/0/test/createapitoken', + headers=headers, data=data) + self.assertEqual(output.status_code, 401) + + def test_api_createapitoken_as_unauthorized_2(self): + """ Test accessing api_project_createapitoken as project user + 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 content + headers = {'Authorization': 'token %s' % mtoken.id} + tdescription = 'my new token' + + # Call the api with pingou user token and verify content + data = { + 'description': tdescription, + 'acl': 'pull_request_merge,pull_request_comment' + } + output = self.app.post('/api/0/test/createapitoken', + headers=headers, data=data) + self.assertEqual(output.status_code, 401) + class PagureFlaskApiProjectConnectorTests(tests.Modeltests): """ Tests for the flask API of pagure for getting connector of a project