From be01e848b0cc6849f39dc0ae04c5e9234293b8b8 Mon Sep 17 00:00:00 2001 From: Karsten Hopp Date: Jul 23 2018 14:27:27 +0000 Subject: Add an API endpoint to add, remove or update ACLs This API endpoint allows any project admin to add, remove or update ACLs on the project. It supports updating ACLs for users and groups. Fixes https://pagure.io/pagure/issue/2747 Merges https://pagure.io/pagure/pull-request/3417 Signed-off-by: Karsten Hopp --- 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)