diff --git a/alembic/versions/2b626a16542e_commit_flag.py b/alembic/versions/2b626a16542e_commit_flag.py new file mode 100644 index 0000000..503cff0 --- /dev/null +++ b/alembic/versions/2b626a16542e_commit_flag.py @@ -0,0 +1,54 @@ +"""commit flag + +Revision ID: 2b626a16542e +Revises: 2fb229dac744 +Create Date: 2017-11-15 10:06:55.088665 + +""" + +import datetime + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '2b626a16542e' +down_revision = '2fb229dac744' + + +def upgrade(): + ''' Create the commit_flags table. ''' + + op.create_table( + 'commit_flags', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('uid', sa.String(32), unique=True, nullable=False), + sa.Column('commit_hash', sa.String(40), index=True, nullable=False), + sa.Column( + 'token_id', sa.String(64), + sa.ForeignKey('tokens.id'), nullable=False), + sa.Column( + 'project_id', + sa.Integer, + sa.ForeignKey( + 'projects.id', onupdate='CASCADE', ondelete='CASCADE', + ), + nullable=False, index=True), + sa.Column( + 'user_id', sa.Integer, + sa.ForeignKey('users.id', onupdate='CASCADE'), + nullable=False, index=True), + sa.Column('username', sa.Text(), nullable=False), + sa.Column('percent', sa.Integer(), nullable=False), + sa.Column('comment', sa.Text(), nullable=False), + sa.Column('url', sa.Text(), nullable=False), + sa.Column( + 'date_created', sa.DateTime, nullable=False, + default=datetime.datetime.utcnow), + ) + + +def downgrade(): + ''' Drop the commit_flags table. ''' + + op.drop_table('commit_flags') diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index 6ce2212..293f0ab 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -90,6 +90,7 @@ class APIERROR(enum.Enum): EMODIFYPROJECTNOTALLOWED = 'You are not allowed to modify this project' EINVALIDPERPAGEVALUE = 'The per_page value must be between 1 and 100' EGITERROR = 'An error occured during a git operation' + ENOCOMMIT = 'No such commit found in this repository' def get_authorized_api_project(SESSION, repo, user=None, namespace=None): @@ -441,6 +442,7 @@ def api(): api_fork_project_doc = load_doc(project.api_fork_project) api_generate_acls_doc = load_doc(project.api_generate_acls) api_new_branch_doc = load_doc(project.api_new_branch) + api_commit_add_flag_doc = load_doc(project.api_commit_add_flag) issues = [] if pagure.APP.config.get('ENABLE_TICKETS', True): @@ -507,7 +509,8 @@ def api(): api_git_branches_doc, api_fork_project_doc, api_generate_acls_doc, - api_new_branch_doc + api_new_branch_doc, + api_commit_add_flag_doc, ], issues=issues, requests=[ diff --git a/pagure/api/project.py b/pagure/api/project.py index b06de7a..0346c69 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -12,9 +12,10 @@ import flask from sqlalchemy.exc import SQLAlchemyError from six import string_types -from pygit2 import GitError +from pygit2 import GitError, Repository import pagure +import pagure.forms import pagure.exceptions import pagure.lib import pagure.lib.git @@ -1267,3 +1268,175 @@ def api_new_branch(repo, username=None, namespace=None): output = {'message': 'Project branch was created'} jsonout = flask.jsonify(output) return jsonout + + +@API.route('//c//flag', methods=['POST']) +@API.route('///c//flag', methods=['POST']) +@API.route('/fork///c//flag', methods=['POST']) +@API.route( + '/fork////c//flag', + methods=['POST']) +@api_login_required(acls=['commit_flag']) +@api_method +def api_commit_add_flag(repo, commit_hash, username=None, namespace=None): + """ + Flag a commit + ------------------- + Add or edit flags on a commit. + + :: + + POST /api/0//c//flag + POST /api/0///c//flag + + :: + + POST /api/0/fork///c//flag + POST /api/0/fork////c//flag + + Input + ^^^^^ + + +---------------+---------+--------------+-----------------------------+ + | Key | Type | Optionality | Description | + +===============+=========+==============+=============================+ + | ``username`` | string | Mandatory | | The name of the | + | | | | application to be | + | | | | presented to users | + | | | | on the pull request page | + +---------------+---------+--------------+-----------------------------+ + | ``percent`` | int | Mandatory | | A percentage of | + | | | | completion compared to | + | | | | the goal. The percentage | + | | | | also determine the | + | | | | background color of the | + | | | | flag on the pull-request | + | | | | page | + +---------------+---------+--------------+-----------------------------+ + | ``comment`` | string | Mandatory | | A short message | + | | | | summarizing the | + | | | | presented results | + +---------------+---------+--------------+-----------------------------+ + | ``url`` | string | Mandatory | | A URL to the result | + | | | | of this flag | + +---------------+---------+--------------+-----------------------------+ + | ``uid`` | string | Optional | | A unique identifier used | + | | | | to identify a flag on a | + | | | | pull-request. If the | + | | | | provided UID matches an | + | | | | existing one, then the | + | | | | API call will update the | + | | | | existing one rather than | + | | | | create a new one. | + | | | | Maximum Length: 32 | + | | | | characters. Default: an | + | | | | auto generated UID | + +---------------+---------+--------------+-----------------------------+ + + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "flag": { + "comment": "Tests passed", + "commit_hash": "62b49f00d489452994de5010565fab81", + "date_created": "1510742565", + "percent": 100, + "url": "http://jenkins.cloud.fedoraproject.org/", + "user": { + "default_email": "bar@pingou.com", + "emails": ["bar@pingou.com", "foo@pingou.com"], + "fullname": "PY C", + "name": "pingou"}, + "username": "Jenkins" + }, + "message": "Flag added", + "uid": "b1de8f80defd4a81afe2e09f39678087" + } + + :: + + { + "flag": { + "comment": "Tests passed", + "commit_hash": "62b49f00d489452994de5010565fab81", + "date_created": "1510742565", + "percent": 100, + "url": "http://jenkins.cloud.fedoraproject.org/", + "user": { + "default_email": "bar@pingou.com", + "emails": ["bar@pingou.com", "foo@pingou.com"], + "fullname": "PY C", + "name": "pingou"}, + "username": "Jenkins" + }, + "message": "Flag updated", + "uid": "b1de8f80defd4a81afe2e09f39678087" + } + + """ # noqa + + repo = get_authorized_api_project( + SESSION, repo, user=username, namespace=namespace) + + output = {} + + if repo is None: + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.ENOPROJECT) + + if flask.g.token.project and repo != flask.g.token.project: + raise pagure.exceptions.APIError( + 401, error_code=APIERROR.EINVALIDTOK) + + reponame = pagure.get_repo_path(repo) + repo_obj = Repository(reponame) + try: + repo_obj.get(commit_hash) + except ValueError: + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.ENOCOMMIT) + + form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False) + if form.validate_on_submit(): + username = form.username.data + percent = form.percent.data + comment = form.comment.data.strip() + url = form.url.data.strip() + uid = form.uid.data.strip() if form.uid.data else None + try: + # New Flag + message, uid = pagure.lib.add_commit_flag( + session=SESSION, + repo=repo, + commit_hash=commit_hash, + username=username, + percent=percent, + comment=comment, + url=url, + uid=uid, + user=flask.g.fas_user.username, + token=flask.g.token.id, + ) + SESSION.commit() + c_flag = pagure.lib.get_commit_flag_by_uid(SESSION, uid) + output['message'] = message + output['uid'] = uid + output['flag'] = c_flag.to_json() + except pagure.exceptions.PagureException as err: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOCODE, error=str(err)) + except SQLAlchemyError as err: # pragma: no cover + 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, errors=form.errors) + + jsonout = flask.jsonify(output) + return jsonout diff --git a/pagure/default_config.py b/pagure/default_config.py index f2e1bb1..f98d19b 100644 --- a/pagure/default_config.py +++ b/pagure/default_config.py @@ -262,7 +262,8 @@ ACLS = { 'issue_update_custom_fields': 'Update the custom fields of an issue', 'issue_update_milestone': 'Update the milestone of an issue', 'modify_project': 'Modify an existing project', - 'generate_acls_project': 'Generate the Gitolite ACLs on a project' + 'generate_acls_project': 'Generate the Gitolite ACLs on a project', + 'commit_flag': 'Flag a commit', } # List of ACLs which a regular user is allowed to associate to an API token @@ -285,7 +286,8 @@ ADMIN_API_ACLS = [ 'pull_request_flag', 'pull_request_comment', 'pull_request_merge', - 'generate_acls_project' + 'generate_acls_project', + 'commit_flag', ] # Bootstrap URLS diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 77e1c57..54ef5f8 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -1319,6 +1319,71 @@ def add_pull_request_flag(session, request, username, percent, comment, url, return ('Flag %s' % action, pr_flag.uid) +def add_commit_flag( + session, repo, commit_hash, username, status, percent, comment, url, + uid, user, token): + ''' Add a flag to a add_commit_flag. ''' + user_obj = get_user(session, user) + + action = 'added' + c_flag = get_commit_flag_by_uid(session, uid) + if c_flag: + action = 'updated' + c_flag.comment = comment + c_flag.percent = percent + c_flag.url = url + else: + c_flag = model.CommitFlag( + uid=uid or uuid.uuid4().hex, + project_id=repo.id, + commit_hash=commit_hash, + username=username, + percent=percent, + comment=comment, + url=url, + user_id=user_obj.id, + token_id=token, + ) + session.add(c_flag) + # Make sure we won't have SQLAlchemy error before we continue + session.flush() + + pagure.lib.notify.log( + repo, + topic='commit.flag.%s' % action, + msg=dict( + repo=repo.to_json(public=True), + flag=c_flag.to_json(public=True), + agent=user_obj.username, + ), + redis=REDIS, + ) + + return ('Flag %s' % action, c_flag.uid) + + +def get_commit_flag(session, project, commit_hash): + ''' Return the commit flags corresponding to the specified git hash + (commitid) in the specified repository. + + :arg session: the session with which to connect to the database + :arg repo: the pagure.lib.model.Project object corresponding to the + project whose commit has been flagged + :arg commit_hash: the hash of the commit who has been flagged + :return: list of pagure.lib.model.CommitFlag objects or an empty list + + ''' + query = session.query( + model.CommitFlag + ).filter( + model.CommitFlag.project_id == project.id + ).filter( + model.CommitFlag.commit_hash == commit_hash + ) + + return query.all() + + def new_project(session, user, name, blacklist, allowed_prefix, gitfolder, docfolder, ticketfolder, requestfolder, description=None, url=None, avatar_email=None, @@ -2811,6 +2876,27 @@ def get_pull_request_flag_by_uid(session, flag_uid): return query.first() +def get_commit_flag_by_uid(session, flag_uid): + ''' Return the flag corresponding to the specified unique identifier. + + :arg session: the session to use to connect to the database. + :arg flag_uid: the unique identifier of a request. This identifier is + unique accross all flags on this pagure instance and should be + unique accross multiple pagure instances as well + :type request_uid: str or None + + :return: A single Issue object. + :rtype: pagure.lib.model.PullRequestFlag + + ''' + query = session.query( + model.CommitFlag + ).filter( + model.CommitFlag.uid == flag_uid.strip() if flag_uid else None + ) + return query.first() + + def set_up_user(session, username, fullname, default_email, emails=None, ssh_key=None, keydir=None): ''' Set up a new user into the database or update its information. ''' diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 236793b..e137112 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -2013,6 +2013,74 @@ class PullRequestFlag(BASE): return output +class CommitFlag(BASE): + """ Stores the flags attached to a commit. + + Table -- commit_flags + """ + + __tablename__ = 'commit_flags' + + id = sa.Column(sa.Integer, primary_key=True) + commit_hash = sa.Column(sa.String(40), index=True, nullable=False) + project_id = sa.Column( + sa.Integer, + sa.ForeignKey( + 'projects.id', onupdate='CASCADE', ondelete='CASCADE', + ), + nullable=False, index=True) + token_id = sa.Column( + sa.String(64), sa.ForeignKey( + 'tokens.id', + ), + nullable=False) + user_id = sa.Column( + sa.Integer, + sa.ForeignKey( + 'users.id', onupdate='CASCADE', + ), + nullable=False, + index=True) + uid = sa.Column(sa.String(32), unique=True, nullable=False) + username = sa.Column( + sa.Text(), + nullable=False) + percent = sa.Column( + sa.Integer(), + nullable=False) + comment = sa.Column( + sa.Text(), + nullable=False) + url = sa.Column( + sa.Text(), + nullable=False) + + date_created = sa.Column(sa.DateTime, nullable=False, + default=datetime.datetime.utcnow) + + user = relation('User', foreign_keys=[user_id], + remote_side=[User.id], + backref=backref( + 'commit_flags', + order_by="CommitFlag.date_created")) + + def to_json(self, public=False): + ''' Returns a dictionnary representation of the commit flag. + + ''' + output = { + 'commit_hash': self.commit_hash, + 'username': self.username, + 'percent': self.percent, + 'comment': self.comment, + 'url': self.url, + 'date_created': self.date_created.strftime('%s'), + 'user': self.user.to_json(public=public), + } + + return output + + class PagureGroupType(BASE): """ A list of the type a group can have definition. diff --git a/pagure/templates/commit.html b/pagure/templates/commit.html index 0534892..00a9c93 100644 --- a/pagure/templates/commit.html +++ b/pagure/templates/commit.html @@ -129,6 +129,31 @@ {% endif %} +
+
+
    + {% for flag in flags %} +
  • +
    + {{ flag.username }} +
    {{ flag.percent }}%
    +
    +
    + {{ flag.comment }} +
    + {{ flag.date_created | humanize }}
    +
    +
    +
  • + {% endfor %} +
+
+
+ {% set filecount = 0 %} {% for patch in diff %} {% set filecount = filecount + 1 %} diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index aa20dc7..5dfc70c 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -753,6 +753,7 @@ def view_commit(repo, commitid, username=None, namespace=None): commit=commit, diff=diff, form=pagure.forms.ConfirmationForm(), + flags=pagure.lib.get_commit_flag(SESSION, repo, commitid), ) diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py index 4665619..1a71fe1 100644 --- a/tests/test_pagure_flask_api_issue.py +++ b/tests/test_pagure_flask_api_issue.py @@ -2522,7 +2522,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): # is required item = pagure.lib.model.TokenAcl( token_id='pingou_foo', - acl_id=5, + acl_id=6, ) self.session.add(item) self.session.commit() diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index 4327b52..9643dcd 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -2713,5 +2713,250 @@ class PagureFlaskApiProjecttests(tests.Modeltests): self.assertEqual(data, expected_output) self.assertIn('test123', repo_obj.listall_branches()) + +class PagureFlaskApiProjectFlagtests(tests.Modeltests): + """ Tests for the flask API of pagure for flagging commit in project + """ + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskApiProjectFlagtests, self).setUp() + + pagure.APP.config['TESTING'] = True + pagure.SESSION = self.session + pagure.api.SESSION = self.session + pagure.api.project.SESSION = self.session + pagure.lib.SESSION = self.session + + tests.create_projects(self.session) + repo_path = os.path.join(self.path, 'repos') + self.git_path = os.path.join(repo_path, 'test.git') + tests.create_projects_git(repo_path, bare=True) + tests.add_content_git_repo(self.git_path) + tests.create_tokens(self.session, project_id=None) + tests.create_tokens_acl( + self.session, 'aaabbbcccddd', 'commit_flag') + + def test_flag_commit_missing_percent(self): + """ Test flagging a commit with missing precentage. """ + repo_obj = pygit2.Repository(self.git_path) + commit = repo_obj.revparse_single('HEAD') + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'username': 'Jenkins', + 'comment': 'Tests passed', + 'url': 'http://jenkins.cloud.fedoraproject.org/', + 'uid': 'jenkins_build_pagure_100+seed', + } + output = self.app.post( + '/api/0/test/c/%s/flag' % commit.oid.hex, + headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + expected_output = { + "error": "Invalid or incomplete input submited", + "error_code": "EINVALIDREQ", + "errors": { + "percent": [ + "This field is required." + ] + } + } + self.assertEqual(data, expected_output) + + def test_flag_commit_missing_username(self): + """ Test flagging a commit with missing username. """ + repo_obj = pygit2.Repository(self.git_path) + commit = repo_obj.revparse_single('HEAD') + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'percent': 100, + 'comment': 'Tests passed', + 'url': 'http://jenkins.cloud.fedoraproject.org/', + 'uid': 'jenkins_build_pagure_100+seed', + } + output = self.app.post( + '/api/0/test/c/%s/flag' % commit.oid.hex, + headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + expected_output = { + "error": "Invalid or incomplete input submited", + "error_code": "EINVALIDREQ", + "errors": { + "username": [ + "This field is required." + ] + } + } + self.assertEqual(data, expected_output) + + def test_flag_commit_missing_comment(self): + """ Test flagging a commit with missing comment. """ + repo_obj = pygit2.Repository(self.git_path) + commit = repo_obj.revparse_single('HEAD') + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'username': 'Jenkins', + 'percent': 100, + 'url': 'http://jenkins.cloud.fedoraproject.org/', + 'uid': 'jenkins_build_pagure_100+seed', + } + output = self.app.post( + '/api/0/test/c/%s/flag' % commit.oid.hex, + headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + expected_output = { + "error": "Invalid or incomplete input submited", + "error_code": "EINVALIDREQ", + "errors": { + "comment": [ + "This field is required." + ] + } + } + self.assertEqual(data, expected_output) + + def test_flag_commit_missing_url(self): + """ Test flagging a commit with missing url. """ + repo_obj = pygit2.Repository(self.git_path) + commit = repo_obj.revparse_single('HEAD') + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'username': 'Jenkins', + 'percent': 100, + 'comment': 'Tests passed', + 'uid': 'jenkins_build_pagure_100+seed', + } + output = self.app.post( + '/api/0/test/c/%s/flag' % commit.oid.hex, + headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + expected_output = { + "error": "Invalid or incomplete input submited", + "error_code": "EINVALIDREQ", + "errors": { + "url": [ + "This field is required." + ] + } + } + self.assertEqual(data, expected_output) + + def test_flag_commit_invalid_token(self): + """ Test flagging a commit with missing info. """ + repo_obj = pygit2.Repository(self.git_path) + commit = repo_obj.revparse_single('HEAD') + + headers = {'Authorization': 'token 123'} + data = { + 'username': 'Jenkins', + 'percent': 100, + 'comment': 'Tests passed', + 'url': 'http://jenkins.cloud.fedoraproject.org/', + 'uid': 'jenkins_build_pagure_100+seed', + } + output = self.app.post( + '/api/0/test/c/%s/flag' % commit.oid.hex, + headers=headers, data=data) + self.assertEqual(output.status_code, 401) + data = json.loads(output.data) + expected_output = { + "error": "Invalid or expired token. Please visit " + "https://pagure.org/ to get or renew your API token.", + "error_code": "EINVALIDTOK" + } + self.assertEqual(data, expected_output) + + def test_flag_commit_with_uid(self): + """ Test flagging a commit with provided uid. """ + repo_obj = pygit2.Repository(self.git_path) + commit = repo_obj.revparse_single('HEAD') + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'username': 'Jenkins', + 'percent': 100, + 'comment': 'Tests passed', + 'url': 'http://jenkins.cloud.fedoraproject.org/', + 'uid': 'jenkins_build_pagure_100+seed', + } + output = self.app.post( + '/api/0/test/c/%s/flag' % commit.oid.hex, + headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.data) + data['flag']['date_created'] = u'1510742565' + data['flag']['commit_hash'] = u'62b49f00d489452994de5010565fab81' + expected_output = { + u'flag': { + u'comment': u'Tests passed', + u'commit_hash': u'62b49f00d489452994de5010565fab81', + u'date_created': u'1510742565', + u'percent': 100, + u'url': u'http://jenkins.cloud.fedoraproject.org/', + u'user': { + u'default_email': u'bar@pingou.com', + u'emails': [u'bar@pingou.com', u'foo@pingou.com'], + u'fullname': u'PY C', + u'name': u'pingou'}, + u'username': u'Jenkins' + }, + u'message': u'Flag added', + u'uid': u'jenkins_build_pagure_100+seed' + } + + self.assertEqual(data, expected_output) + + def test_flag_commit_without_uid(self): + """ Test flagging a commit with missing info. """ + repo_obj = pygit2.Repository(self.git_path) + commit = repo_obj.revparse_single('HEAD') + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'username': 'Jenkins', + 'percent': 100, + 'comment': 'Tests passed', + 'url': 'http://jenkins.cloud.fedoraproject.org/', + } + output = self.app.post( + '/api/0/test/c/%s/flag' % commit.oid.hex, + headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.data) + self.assertNotEqual( + data['uid'], + u'jenkins_build_pagure_100+seed' + ) + data['flag']['date_created'] = u'1510742565' + data['flag']['commit_hash'] = u'62b49f00d489452994de5010565fab81' + data['uid'] = 'b1de8f80defd4a81afe2e09f39678087' + expected_output = { + u'flag': { + u'comment': u'Tests passed', + u'commit_hash': u'62b49f00d489452994de5010565fab81', + u'date_created': u'1510742565', + u'percent': 100, + u'url': u'http://jenkins.cloud.fedoraproject.org/', + u'user': { + u'default_email': u'bar@pingou.com', + u'emails': [u'bar@pingou.com', u'foo@pingou.com'], + u'fullname': u'PY C', + u'name': u'pingou'}, + u'username': u'Jenkins' + }, + u'message': u'Flag added', + u'uid': u'b1de8f80defd4a81afe2e09f39678087' + } + self.assertEqual(data, expected_output) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index 9f17980..0247854 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -5482,6 +5482,7 @@ foo bar self.assertEqual( [a.name for a in acls], [ + 'commit_flag', 'create_project', 'fork_project', 'generate_acls_project',