From 9f9b1ed839be4dd33dbb150e7aee730b734f9d03 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 1/10] Add a new API endpoint to create a new project via the API --- diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index d3294e7..b05cda2 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -72,6 +72,8 @@ class APIERROR(enum.Enum): ENOTASSIGNED = 'This request must be assigned to be merged' ENOUSER = 'No such user found' ENOCOMMENT = 'Comment not found' + ENEWPROJECTDISABLED = 'Creating project have been disabled for this '\ + 'instance' def check_api_acls(acls, optional=False): diff --git a/pagure/api/project.py b/pagure/api/project.py index f6c13a2..1fe9d02 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -10,11 +10,13 @@ import flask +from sqlalchemy.exc import SQLAlchemyError + import pagure import pagure.exceptions import pagure.lib -from pagure import SESSION -from pagure.api import API, api_method, APIERROR +from pagure import SESSION, APP +from pagure.api import API, api_method, APIERROR, api_login_required @API.route('//git/tags') @@ -163,3 +165,104 @@ def api_projects(): 'projects': [p.to_json(api=True, public=True) for p in projects] }) return jsonout + + +@API.route('/new/', methods=['POST']) +@API.route('/new', methods=['POST']) +@api_login_required(acls=['create_project']) +@api_method +def api_new_project(): + """ + Create a new project + -------------------- + Create a new project on this pagure instance. + + :: + + POST /api/0//new + + + Input + ^^^^^ + + +------------------+---------+--------------+---------------------------+ + | Key | Type | Optionality | Description | + +==================+=========+==============+===========================+ + | ``name`` | string | Mandatory | | The name of the new | + | | | | project. | + +------------------+---------+--------------+---------------------------+ + | ``description`` | string | Mandatory | | A short description of | + | | | | the new project. | + +------------------+---------+--------------+---------------------------+ + | ``url`` | string | Optional | | An url providing more | + | | | | information about the | + | | | | project. | + +------------------+---------+--------------+---------------------------+ + | ``avatar_email`` | string | Optional | | An email address for the| + | | | | avatar of the project. | + +------------------+---------+--------------+---------------------------+ + | ``create_readme``| boolean | Optional | | A boolean to specify if | + | | | | there should be a readme| + | | | | added to the project on | + | | | | creation. | + +------------------+---------+--------------+---------------------------+ + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + 'message': 'Project "foo" created' + } + + """ + user = pagure.lib.search_user(SESSION, username=flask.g.fas_user.username) + output = {} + + if not pagure.APP.config.get('ENABLE_NEW_PROJECTS', True): + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.ENEWPROJECTDISABLED) + + form = pagure.forms.ProjectForm(csrf_enabled=False) + if form.validate_on_submit(): + name = form.name.data + description = form.description.data + url = form.url.data + avatar_email = form.avatar_email.data + create_readme = form.create_readme.data + + try: + message = pagure.lib.new_project( + SESSION, + name=name, + description=description, + url=url, + avatar_email=avatar_email, + user=flask.g.fas_user.username, + blacklist=APP.config['BLACKLISTED_PROJECTS'], + allowed_prefix=APP.config['ALLOWED_PREFIX'], + gitfolder=APP.config['GIT_FOLDER'], + docfolder=APP.config['DOCS_FOLDER'], + ticketfolder=APP.config['TICKETS_FOLDER'], + requestfolder=APP.config['REQUESTS_FOLDER'], + add_readme=create_readme, + userobj=user, + ) + SESSION.commit() + pagure.lib.git.generate_gitolite_acls() + output['message'] = message + except pagure.exceptions.PagureException as err: + print err, str(err) + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOCODE, error=str(err)) + except SQLAlchemyError as err: # pragma: no cover + print err + APP.logger.exception(err) + SESSION.rollback() + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + else: + raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ) + + jsonout = flask.jsonify(output) + return jsonout From 78b2538f9ca489c1cbcd39abc64602ec6589d69c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 2/10] Show the new API endpoint in the API documentation --- diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index b05cda2..d0a1dc7 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -433,6 +433,8 @@ def api(): fork.api_pull_request_add_comment) api_pull_request_add_flag_doc = load_doc(fork.api_pull_request_add_flag) + api_new_project_doc = load_doc(project.api_new_project) + api_version_doc = load_doc(api_version) api_users_doc = load_doc(api_users) api_view_user_doc = load_doc(user.api_view_user) @@ -454,6 +456,7 @@ def api(): version=__api_version__.split('.'), api_doc=APIDOC, projects=[ + api_new_project_doc, api_git_tags_doc, api_projects_doc, ], From 1811318543e7ea3406e7c59d23cc6e74d3cb2c50 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 3/10] Fix returning ENOCODE error as otherwise the object is not JSON serializable --- diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index d0a1dc7..8dbd83a 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -174,7 +174,7 @@ def api_method(function): response = flask.jsonify( { 'error': e.error, - 'error_code': e.error_code + 'error_code': e.error_code.name } ) else: From f7cee5d79cb1ed64b8545f81d182ebf544d5a117 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 4/10] Add the new ACL 'create_project' that can be associated to API tokens --- diff --git a/pagure/default_config.py b/pagure/default_config.py index 025b27c..218639d 100644 --- a/pagure/default_config.py +++ b/pagure/default_config.py @@ -192,6 +192,7 @@ BLACKLISTED_GROUPS = ['forks'] ACLS = { + 'create_project': 'Create a new project', 'issue_create': 'Create a new ticket against this project', 'issue_change_status': 'Change the status of a ticket of this project', 'issue_comment': 'Comment on a ticket of this project', From 6b2dc410b16f84e1649c654cb7a98d9509eb9f56 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 5/10] Ensure when loading the ACLs to the DB, we keep the order the same This is most useful for the tests ensuring the acl ID 1 is always the same --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index bf4167f..4ba212f 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -123,7 +123,7 @@ def create_default_status(session, acls=None): session.rollback() ERROR_LOG.debug('Type %s could not be added', grptype) - for acl in acls or {}: + for acl in sorted(acls) or {}: item = ACL( name=acl, description=acls[acl] From d2dd034fa1259925d13d495da88f1ba1e638a5cb Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 6/10] Adjust the unit-tests for a change in identifier in one of the ACLs --- diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py index 84e8f73..6739883 100644 --- a/tests/test_pagure_flask_api_issue.py +++ b/tests/test_pagure_flask_api_issue.py @@ -921,10 +921,11 @@ class PagureFlaskApiIssuetests(tests.Modeltests): self.session.add(item) self.session.commit() - # Give `change_status_issue` to this token + # Give `issue_change_status` to this token when `issue_comment` + # is required item = pagure.lib.model.TokenAcl( token_id='pingou_foo', - acl_id=1, + acl_id=2, ) self.session.add(item) self.session.commit() From ef3813ac0a0b68967cecccb46749d26747c7fb36 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 7/10] Fix unit-tests in the API endpoints for issues The token provided there didn't have all the right ACLs so tests were failing. This fixes it --- diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py index 6739883..aa59574 100644 --- a/tests/test_pagure_flask_api_issue.py +++ b/tests/test_pagure_flask_api_issue.py @@ -779,13 +779,14 @@ class PagureFlaskApiIssuetests(tests.Modeltests): # Un-authorized issue output = self.app.post( '/api/0/foo/issue/1/status', data=data, headers=headers) - self.assertEqual(output.status_code, 403) + self.assertEqual(output.status_code, 401) data = json.loads(output.data) self.assertDictEqual( data, { - "error": "You are not allowed to view this issue", - "error_code": "EISSUENOTALLOWED", + "error": "Invalid or expired token. Please " + "visit https://pagure.org/ to get or renew your API token.", + "error_code": "EINVALIDTOK" } ) @@ -958,13 +959,14 @@ class PagureFlaskApiIssuetests(tests.Modeltests): # Valid request but un-authorized output = self.app.post( '/api/0/foo/issue/1/comment', data=data, headers=headers) - self.assertEqual(output.status_code, 403) + self.assertEqual(output.status_code, 401) data = json.loads(output.data) self.assertDictEqual( data, { - "error": "You are not allowed to view this issue", - "error_code": "EISSUENOTALLOWED", + "error": "Invalid or expired token. Please " + "visit https://pagure.org/ to get or renew your API token.", + "error_code": "EINVALIDTOK" } ) From c8ee9f839fdad2f6f93a031143ebf34e999b6297 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 8/10] Give all the ACLs to the specified token now that there is an 8th one --- diff --git a/tests/__init__.py b/tests/__init__.py index f39b1d9..0746c1a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -263,7 +263,7 @@ def create_tokens(session, user_id=1): def create_tokens_acl(session, token_id='aaabbbcccddd'): """ Create some acls for the tokens. """ - for aclid in range(7): + for aclid in range(8): item = pagure.lib.model.TokenAcl( token_id=token_id, acl_id=aclid + 1, From 1c1b5dea0261bb2ca5acbf3e5dcd74a6b3582cd8 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 9/10] Add unit-test for the API endpoint to create project --- diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index a2675e0..f85adf5 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -44,8 +44,15 @@ class PagureFlaskApiProjecttests(tests.Modeltests): pagure.api.project.SESSION = self.session pagure.lib.SESSION = self.session - pagure.APP.config['REQUESTS_FOLDER'] = None - pagure.APP.config['GIT_FOLDER'] = os.path.join(tests.HERE, 'repos') + pagure.APP.config['GIT_FOLDER'] = os.path.join(tests.HERE, 'repos') + pagure.APP.config['FORK_FOLDER'] = os.path.join( + tests.HERE, 'forks') + pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( + tests.HERE, 'requests') + pagure.APP.config['TICKETS_FOLDER'] = os.path.join( + tests.HERE, 'tickets') + pagure.APP.config['DOCS_FOLDER'] = os.path.join( + tests.HERE, 'docs') self.app = pagure.APP.test_client() @@ -222,6 +229,95 @@ class PagureFlaskApiProjecttests(tests.Modeltests): } ) + @patch('pagure.lib.git.generate_gitolite_acls') + def test_api_new_project(self, p_gga): + """ Test the api_new_project method of the flask api. """ + p_gga.return_value = True + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(tests.HERE, 'tickets')) + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + headers = {'Authorization': 'token foo_token'} + + # Invalid token + output = self.app.post('/api/0/new', headers=headers) + self.assertEqual(output.status_code, 401) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + "error": "Invalid or expired token. Please visit " \ + "https://pagure.org/ to get or renew your API token.", + "error_code": "EINVALIDTOK" + } + ) + + headers = {'Authorization': 'token aaabbbcccddd'} + + # No input + output = self.app.post('/api/0/new', headers=headers) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + "error": "Invalid or incomplete input submited", + "error_code": "EINVALIDREQ", + } + ) + + data = { + 'name': 'test', + } + + # Incomplete request + output = self.app.post( + '/api/0/new', data=data, headers=headers) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + "error": "Invalid or incomplete input submited", + "error_code": "EINVALIDREQ", + } + ) + + data = { + 'name': 'test', + 'description': 'Just a small test project', + } + + # Valid request but repo already exists + output = self.app.post( + '/api/0/new/', data=data, headers=headers) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + "error": "The tickets repo \"test.git\" already exists", + "error_code": "ENOCODE" + } + ) + + data = { + 'name': 'test_42', + 'description': 'Just another small test project', + } + + # Valid request + output = self.app.post( + '/api/0/new/', data=data, headers=headers) + self.assertEqual(output.status_code, 200) + data = json.loads(output.data) + self.assertDictEqual( + data, + {'message': 'Project "test_42" created'} + ) + if __name__ == '__main__': SUITE = unittest.TestLoader().loadTestsFromTestCase( From 06e56b4c9b756f9558fcaf918bcbdeaf6e1eaf53 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 07 2016 13:15:10 +0000 Subject: [PATCH 10/10] Fix the ACL id now that ACLs are inserted in a specific order --- diff --git a/tests/test_pagure_flask_api_fork.py b/tests/test_pagure_flask_api_fork.py index eb2185c..9c689ea 100644 --- a/tests/test_pagure_flask_api_fork.py +++ b/tests/test_pagure_flask_api_fork.py @@ -368,7 +368,7 @@ class PagureFlaskApiForktests(tests.Modeltests): self.session.commit() item = pagure.lib.model.TokenAcl( token_id='foobar_token', - acl_id=2, + acl_id=5, ) self.session.add(item) self.session.commit() @@ -480,7 +480,7 @@ class PagureFlaskApiForktests(tests.Modeltests): self.session.commit() item = pagure.lib.model.TokenAcl( token_id='foobar_token', - acl_id=3, + acl_id=8, ) self.session.add(item) self.session.commit()