From 7908423fe24703153b364e4646ca45e7d7e55282 Mon Sep 17 00:00:00 2001
From: Karsten Hopp <karsten@redhat.com>
Date: Jul 23 2018 12:49:22 +0000
Subject: add API endpoint to change ACLs

add acl for groups
add tests for modify ACL API
check permissions
add test for user without permission
switch to using forms

Signed-off-by: Karsten Hopp <karsten@redhat.com>

---

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..0b73f2a 100644
--- a/pagure/api/project.py
+++ b/pagure/api/project.py
@@ -1713,3 +1713,179 @@ def api_update_project_watchers(repo, username=None, namespace=None):
             400, error_code=APIERROR.EDBERROR)
 
     return flask.jsonify({'message': msg, 'status': 'ok'})
+
+
+@API.route('/<repo>/git/modifyacls', methods=['POST'])
+@API.route('/<namespace>/<repo>/git/modifyacls', methods=['POST'])
+@API.route('/fork/<username>/<repo>/git/modifyacls', methods=['POST'])
+@API.route('/fork/<username>/<namespace>/<repo>/git/modifyacls',
+           methods=['POST'])
+@api_login_required(acls=['modify_project'])
+@api_method
+def api_modify_acls(repo, namespace=None, username=None, user=None,
+                    group=None, acls=None):
+    """
+    Modify ACLs on a project
+    -----------------------------------
+    Modify ACLs on a project. This is restricted to project admins.
+
+    ::
+
+        POST /api/0/<repo>/modifyacls/flag
+        POST /api/0/<namespace>/<repo>/modifyacls/flag
+
+    ::
+
+        POST /api/0/fork/<username>/<repo>/modifyacls/flag
+        POST /api/0/fork/<username>/<namespace>/<repo>/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 <span class="error">*</span>',
+        [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 <pingou@pingoured.fr>
+   Karsten Hopp <karsten@redhat.com>
 
 """
 
@@ -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)

