diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index 394a33e..e014020 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -500,6 +500,7 @@ def api(): api_new_project_doc = load_doc(project.api_new_project) api_modify_project_doc = load_doc(project.api_modify_project) api_fork_project_doc = load_doc(project.api_fork_project) + api_modify_acls_doc = load_doc(project.api_modify_acls) api_generate_acls_doc = load_doc(project.api_generate_acls) api_new_branch_doc = load_doc(project.api_new_branch) api_commit_flags_doc = load_doc(project.api_commit_flags) @@ -578,6 +579,7 @@ def api(): api_project_watchers_doc, api_git_branches_doc, api_fork_project_doc, + api_modify_acls_doc, api_generate_acls_doc, api_new_branch_doc, api_commit_flags_doc, diff --git a/pagure/api/project.py b/pagure/api/project.py index 5ef3a90..2f22e95 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -1713,3 +1713,180 @@ def api_update_project_watchers(repo, username=None, namespace=None): 400, error_code=APIERROR.EDBERROR) return flask.jsonify({'message': msg, 'status': 'ok'}) + + +@API.route('//git/modifyacls', methods=['POST']) +@API.route('///git/modifyacls', methods=['POST']) +@API.route('/fork///git/modifyacls', methods=['POST']) +@API.route('/fork////git/modifyacls', + methods=['POST']) +@api_login_required(acls=['modify_project']) +@api_method +def api_modify_acls(repo, namespace=None, username=None): + """ + Modify ACLs on a project + ------------------------ + Add, remove or update ACLs on a project for a particular user or group. + + This is restricted to project admins. + + :: + + POST /api/0//modifyacls/flag + POST /api/0///modifyacls/flag + + :: + + POST /api/0/fork///modifyacls/flag + POST /api/0/fork////modifyacls/flag + + + Input + ^^^^^ + + +------------------+---------+---------------+---------------------------+ + | Key | Type | Optionality | Description | + +==================+=========+===============+===========================+ + | ``user_type`` | String | Mandatory | A string to specify if | + | | | | the ACL should be changed | + | | | | for a user or a group. | + | | | | Specifying one of either | + | | | | 'user' or 'group' is | + | | | | mandatory | + | | | | | + +------------------+---------+---------------+---------------------------+ + | ``name`` | String | Mandatory | The name of the user or | + | | | | group whose ACL | + | | | | should be changed. | + | | | | | + +------------------+---------+---------------+---------------------------+ + | ``acl`` | String | Mandatory | can be either | + | | | | 'ticket', 'commit', | + | | | | 'admin'. | + | | | | | + +------------------+---------+---------------+---------------------------+ + + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "access_groups": { + "admin": [], + "commit": [], + "ticket": [] + }, + "access_users": { + "admin": [], + "commit": [ + "ta2" + ], + "owner": [ + "karsten" + ], + "ticket": [ + "ta1" + ] + }, + "close_status": [], + "custom_keys": [], + "date_created": "1531131619", + "date_modified": "1531302337", + "description": "pagure local instance", + "fullname": "pagure", + "id": 1, + "milestones": {}, + "name": "pagure", + "namespace": null, + "parent": null, + "priorities": {}, + "tags": [], + "url_path": "pagure", + "user": { + "fullname": "KH", + "name": "karsten" + } + } + + """ + 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) + + is_site_admin = pagure.utils.is_admin() + admins = [u.username for u in project.get_project_users('admin')] + if flask.g.fas_user.username not in admins \ + and flask.g.fas_user.username != project.user.username \ + and not is_site_admin: + raise pagure.exceptions.APIError( + 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED) + + form = pagure.forms.ModifyACLForm(csrf_enabled=False) + if form.validate_on_submit(): + if form.user_type.data == 'user': + user = form.name.data + group = None + else: + group = form.name.data + user = None + acl = form.acl.data + + if user: + user_obj = pagure.lib.search_user(flask.g.session, username=user) + if not user_obj: + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.ENOUSER) + + elif group: + group_obj = pagure.lib.search_groups( + flask.g.session, group_name=group) + if not group_obj: + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.ENOGROUP) + + if user and user_obj not in project.access_users[acl] and \ + user_obj != project.user.user: + msg = pagure.lib.add_user_to_project( + session=flask.g.session, + project=project, + new_user=user, + user=flask.g.fas_user.username, + access=acl + ) + elif group and group_obj not in project.access_groups[acl]: + msg = pagure.lib.add_group_to_project( + session=flask.g.session, + project=project, + new_group=group, + user=flask.g.fas_user.username, + access=acl, + create=pagure_config.get('ENABLE_GROUP_MNGT', False), + is_admin=pagure.utils.is_admin(), + ) + try: + flask.g.session.commit() + except pagure.exceptions.PagureException as msg: + flask.g.session.rollback() + _log.debug(msg) + flask.flash(str(msg), 'error') + except SQLAlchemyError as err: + _log.exception(err) + flask.g.session.rollback() + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + pagure.lib.git.generate_gitolite_acls(project=project) + output = project.to_json(api=True, public=True) + else: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors) + + jsonout = flask.jsonify(output) + return jsonout diff --git a/pagure/forms.py b/pagure/forms.py index 54f11be..c63e1bd 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -587,6 +587,25 @@ class ConfirmationForm(PagureForm): pass +class ModifyACLForm(PagureForm): + ''' Form to change ACL of a user or a group to a project. ''' + user_type = wtforms.SelectField( + 'User type', + [wtforms.validators.Required()], + choices=[('user', 'User'), ('group', 'Group')] + ) + name = wtforms.TextField( + 'User- or Groupname *', + [wtforms.validators.Required()] + ) + acl = wtforms.SelectField( + 'ACL type', + [wtforms.validators.Required()], + choices=[('admin', 'Admin'), ('ticket', 'Ticket'), + ('commit', 'Commit')] + ) + + class UploadFileForm(PagureForm): ''' Form to upload a file. ''' filestream = wtforms.FileField( diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index 4faa79f..b16b083 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -5,6 +5,7 @@ Authors: Pierre-Yves Chibon + Karsten Hopp """ @@ -2625,6 +2626,278 @@ class PagureFlaskApiProjecttests(tests.Modeltests): } self.assertEqual(data, expected_output) + def test_api_modify_acls_no_project(self): + """ Test the api_modify_acls method of the flask api when the project + doesn't exist """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + data = { + 'user_type': 'user', + 'name': 'bar', + 'acl': 'commit' + } + output = self.app.post( + '/api/0/test12345123/git/modifyacls', + headers=headers, data=data) + self.assertEqual(output.status_code, 404) + data = json.loads(output.get_data(as_text=True)) + expected_output = { + 'error_code': 'ENOPROJECT', + 'error': 'Project not found' + } + self.assertEqual(data, expected_output) + + def test_api_modify_acls_no_user(self): + """ Test the api_modify_acls method of the flask api when the user + doesn't exist """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + data = { + 'user_type': 'user', + 'name': 'nosuchuser', + 'acl': 'commit' + } + output = self.app.post( + '/api/0/test/git/modifyacls', + headers=headers, data=data) + self.assertEqual(output.status_code, 404) + data = json.loads(output.get_data(as_text=True)) + expected_output = { + 'error': 'No such user found', + 'error_code': u'ENOUSER' + } + self.assertEqual(data, expected_output) + + def test_api_modify_acls_no_group(self): + """ Test the api_modify_acls method of the flask api when the group + doesn't exist """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + data = { + 'user_type': 'group', + 'name': 'nosuchgroup', + 'acl': 'commit' + } + output = self.app.post( + '/api/0/test/git/modifyacls', + headers=headers, data=data) + self.assertEqual(output.status_code, 404) + data = json.loads(output.get_data(as_text=True)) + expected_output = { + 'error': 'Group not found', + 'error_code': 'ENOGROUP' + } + self.assertEqual(data, expected_output) + + def test_api_modify_acls_no_permission(self): + """ Test the api_modify_acls method of the flask api when the user + doesn't have permissions """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None, user_id=2) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + data = { + 'user_type': 'user', + 'name': 'foo', + 'acl': 'commit' + } + output = self.app.post( + '/api/0/test/git/modifyacls', + headers=headers, data=data) + self.assertEqual(output.status_code, 401) + data = json.loads(output.get_data(as_text=True)) + expected_output = { + 'error': 'You are not allowed to modify this project', + 'error_code': 'EMODIFYPROJECTNOTALLOWED' + } + self.assertEqual(data, expected_output) + + def test_api_modify_acls_neither_user_nor_group(self): + """ Test the api_modify_acls method of the flask api when neither + user nor group was set """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + data = { + 'acl': 'commit' + } + output = self.app.post( + '/api/0/test/git/modifyacls', + headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.get_data(as_text=True)) + expected_output = { + 'error': 'Invalid or incomplete input submitted', + 'error_code': 'EINVALIDREQ', + 'errors': {'name': ['This field is required.'], + 'user_type': ['Not a valid choice']} + } + self.assertEqual(data, expected_output) + + def test_api_modify_acls_invalid_acl(self): + """ Test the api_modify_acls method of the flask api when the ACL + doesn't exist. Must be one of ticket, commit or admin. """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + data = { + 'user_type': 'user', + 'name': 'bar', + 'acl': 'invalidacl' + } + output = self.app.post( + '/api/0/test/git/modifyacls', + headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.get_data(as_text=True)) + expected_output = { + 'error': 'Invalid or incomplete input submitted', + 'error_code': 'EINVALIDREQ', + 'errors': { + 'acl': ['Not a valid choice'] + } + } + self.assertEqual(data, expected_output) + + def test_api_modify_acls_user(self): + """ Test the api_modify_acls method of the flask api for + setting an ACL for a user. """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + data = { + 'user_type': 'user', + 'name': 'foo', + 'acl': 'commit' + } + output = self.app.post( + '/api/0/test/git/modifyacls', + headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + data['date_created'] = '1510742565' + data['date_modified'] = '1510742566' + + expected_output = { + 'access_groups': {'admin': [], 'commit': [], 'ticket': []}, + 'access_users': {'admin': [], + 'commit': ['foo'], + 'owner': ['pingou'], + 'ticket': []}, + 'close_status': + ['Invalid', 'Insufficient data', 'Fixed', 'Duplicate'], + 'custom_keys': [], + 'date_created': '1510742565', + 'date_modified': '1510742566', + 'description': 'test project #1', + 'fullname': 'test', + 'id': 1, + 'milestones': {}, + 'name': 'test', + 'namespace': None, + 'parent': None, + 'priorities': {}, + 'tags': [], + 'url_path': 'test', + 'user': {'fullname': 'PY C', 'name': 'pingou'} + } + self.assertEqual(data, expected_output) + + def test_api_modify_acls_group(self): + """ Test the api_modify_acls method of the flask api for + setting an ACL for a group. """ + tests.create_projects(self.session) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'modify_project') + headers = {'Authorization': 'token aaabbbcccddd'} + + # Create a group + msg = pagure.lib.add_group( + self.session, + group_name='baz', + display_name='baz group', + description=None, + group_type='bar', + user='foo', + is_admin=False, + blacklist=[], + ) + self.session.commit() + self.assertEqual(msg, 'User `foo` added to the group `baz`.') + + data = { + 'user_type': 'group', + 'name': 'baz', + 'acl': 'ticket' + } + output = self.app.post( + '/api/0/test/git/modifyacls', + headers=headers, data=data) + + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + data['date_created'] = '1510742565' + data['date_modified'] = '1510742566' + + expected_output = { + 'access_groups': { + 'admin': [], + 'commit': [], + 'ticket': ['baz'] + }, + 'access_users': { + 'admin': [], + 'commit': [], + 'owner': ['pingou'], + 'ticket': [] + }, + 'close_status': [ + 'Invalid', + 'Insufficient data', + 'Fixed', + 'Duplicate' + ], + 'custom_keys': [], + 'date_created': '1510742565', + 'date_modified': '1510742566', + 'description': 'test project #1', + 'fullname': 'test', + 'id': 1, + 'milestones': {}, + 'name': 'test', + 'namespace': None, + 'parent': None, + 'priorities': {}, + 'tags': [], + 'url_path': 'test', + 'user': {'fullname': 'PY C', 'name': 'pingou'} + } + self.assertEqual(data, expected_output) + def test_api_new_git_branch(self): """ Test the api_new_branch method of the flask api """ tests.create_projects(self.session) @@ -2648,7 +2921,6 @@ class PagureFlaskApiProjecttests(tests.Modeltests): repo_obj = pygit2.Repository(git_path) self.assertIn('test123', repo_obj.listall_branches()) - def test_api_new_git_branch_json(self): """ Test the api_new_branch method of the flask api """ tests.create_projects(self.session)