From 355f95ce6a468acf03e7dd40bb57660ccb27bc0b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 06 2017 20:00:34 +0000 Subject: [PATCH 1/5] Split generate_gitolite_acls into two method The first one generates the command to be run, the second actually runs it. This allows displaying which command is going to be run without necessarily running it. --- diff --git a/pagure/lib/git.py b/pagure/lib/git.py index 40c41da..d881301 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -137,12 +137,10 @@ def write_gitolite_acls(session, configfile): stream.write(row + '\n') -def generate_gitolite_acls(): - """ Generate the gitolite configuration file for all repos +def _get_gitolite_command(): + """ Return the gitolite command to run based on the info in the + configuration file. """ - pagure.lib.git.write_gitolite_acls( - pagure.SESSION, pagure.APP.config['GITOLITE_CONFIG']) - gitolite_folder = pagure.APP.config.get('GITOLITE_HOME', None) gitolite_version = pagure.APP.config.get('GITOLITE_VERSION', 3) if gitolite_folder: @@ -161,6 +159,17 @@ def generate_gitolite_acls(): raise pagure.exceptions.PagureException( 'Non-supported gitolite version "%s"' % gitolite_version ) + return cmd + + +def generate_gitolite_acls(): + """ Generate the gitolite configuration file for all repos + """ + pagure.lib.git.write_gitolite_acls( + pagure.SESSION, pagure.APP.config['GITOLITE_CONFIG']) + + cmd = _get_gitolite_command() + if cmd: subprocess.Popen( cmd, shell=True, From 079c2a6b56781e63458de0c4d8386325c9bef229 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 06 2017 20:00:34 +0000 Subject: [PATCH 2/5] Introduce pagure-admin This is a CLI tool which will replace the /admin endpoint in the web UI. --- diff --git a/pagure/cli/__init__.py b/pagure/cli/__init__.py new file mode 100644 index 0000000..4212bc1 --- /dev/null +++ b/pagure/cli/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +""" + (c) 2017 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" diff --git a/pagure/cli/admin.py b/pagure/cli/admin.py new file mode 100644 index 0000000..1cbad23 --- /dev/null +++ b/pagure/cli/admin.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +""" + (c) 2017 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" +from __future__ import print_function + +import argparse +import logging + +import pagure.exceptions +import pagure.lib +import pagure.lib.git +from pagure import (SESSION, generate_user_key_files) + + +_log = logging.getLogger(__name__) + + +def parse_arguments(): + """ Set-up the argument parsing. """ + parser = argparse.ArgumentParser( + description='The admin CLI for this pagure instance') + + parser.add_argument( + '--debug', default=False, action='store_true', + help='Increase the verbosity of the information displayed') + parser.set_defaults(func=lambda a, k : print(parser.format_help())) + + subparsers = parser.add_subparsers(title='actions') + + # refresh-gitolite + parser_gitolite = subparsers.add_parser( + 'refresh-gitolite', + help='Re-generate the gitolite confi file') + parser_gitolite.set_defaults(func=do_generate_acl) + + # refresh-ssh + parser_ssh = subparsers.add_parser( + 'refresh-ssh', + help='Re-write to disk every user\'s ssh key stored in the database') + parser_ssh.set_defaults(func=do_refresh_ssh) + + # clear-hook-token + parser_hook_token = subparsers.add_parser( + 'clear-hook-token', + help='Generate a new hook token for every project in this instance') + parser_hook_token.set_defaults(func=do_generate_hook_token) + + return parser.parse_args() + + +def _ask_confirmation(): + ''' Ask to confirm an action + ''' + action = raw_input('Do you want to continue? [y/N]') + return action.lower() in ['y', 'yes'] + + +def do_generate_acl(): + """ Regenerate the gitolite ACL file. """ + cmd = pagure.lib.git._get_gitolite_command() + if not cmd: + raise pagure.exceptions.PagureException( + '/!\ un-able to generate the right gitolite command') + print('Do you want to re-generate the gitolite.conf file then ' + 'calling: %s' % cmd) + if _ask_confirmation(): + pagure.lib.git.generate_gitolite_acls() + print('Gitolite ACLs updated') + + +def do_refresh_ssh(): + """ Regenerate the user key files. """ + print('Do you want to re-generate all the ssh keys for every user in ' + 'the database? (Depending on your instance this may take a while ' + 'and result in an outage while it lasts)') + if _ask_confirmation(): + generate_user_key_files() + print('User key files regenerated') + do_generate_acl() + + +def do_generate_hook_token(): + """ Regenerate the hook_token for each projects in the DB. """ + print('Do you want to re-generate all the hook token for every user in ' + 'the database? This will break every web-hook set-up on this ' + 'instance. You should only ever run this for a security issue') + if _ask_confirmation(): + pagure.lib.generate_hook_token(SESSION) + print('Hook token all re-generated') + + +def main(): + """ Start of the application. """ + + # TODO: figure out if the user is allowed to run this tool at all + # -> require root? + # -> check if pagure's config file is readable? + # -> Ask for something private in pagure's config file? + + # Parse the arguments + args = parse_arguments() + + logging.basicConfig() + if args.debug: + _log.setLevel(logging.DEBUG) + + # Act based on the arguments given + return_code = 0 + try: + args.func() + except KeyboardInterrupt: + print("\nInterrupted by user.") + return_code = 1 + except pagure.exceptions.PagureException as err: + print(err) + return_code = 3 + except Exception as err: + print('Error: {0}'.format(err)) + logging.exception("Generic error catched:") + return_code = 2 + + return return_code + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/setup.py b/setup.py index 9176dce..bcb139d 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,8 @@ setup( entry_points=""" [pygments.styles] diffstyle = pagure.ui.diff_style:DiffStyle + [console_scripts] + pagure-admin=pagure.cli.admin:main """, classifiers=[ 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', From c659c2db1e89b93a1cafe440dcc1e82226c264ba Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 06 2017 20:00:34 +0000 Subject: [PATCH 3/5] Remove the admin interface in pagure This is now replaced by the pagure-admin CLI tool. Adjust the master template for this change. Drop the unit-tests corresponding to this part of the flask app. --- diff --git a/pagure/__init__.py b/pagure/__init__.py index 2475236..71a8e92 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -757,7 +757,6 @@ urlpattern = re.compile(urlregex) # Import the application import pagure.ui.app -import pagure.ui.admin import pagure.ui.fork import pagure.ui.groups if APP.config.get('ENABLE_TICKETS', True): diff --git a/pagure/templates/admin_index.html b/pagure/templates/admin_index.html deleted file mode 100644 index aecb740..0000000 --- a/pagure/templates/admin_index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "master.html" %} - -{% block title %}Home{% endblock %} -{% set tag = "admin" %} - - -{% block content %} - -

Admin section

- -
    -
  • -
    - - {{ form.csrf_token }} -
    -
  • -
  • -
    - - {{ form.csrf_token }} -
    -
  • -
  • -
    - - {{ form.csrf_token }} -
    -

    - This action can be used in case you need to change the hook-token for - *all* the projects hosted on this pagure instance. This can be due to - security reason. Beware that using it will break every web-hook - consumer of every project hosted. -

    -
  • -
  • - - Groups/Users management - -
  • -
- -{% endblock %} diff --git a/pagure/templates/master.html b/pagure/templates/master.html index cfc56b4..333a736 100644 --- a/pagure/templates/master.html +++ b/pagure/templates/master.html @@ -39,11 +39,6 @@ - {% if admin %} - - {% endif %} {% if config.get('ENABLE_NEW_PROJECTS', True) %}
  • Gitolite ACLs updated
  • ' - in output.data) - - csrf_token = output.data.split( - 'name="csrf_token" type="hidden" value="')[1].split('">')[0] - - data = {'csrf_token': csrf_token} - output = self.app.post( - '/admin/gitolite', data=data, follow_redirects=True) - self.assertEqual(output.status_code, 200) - self.assertTrue('

    Admin section

    ' in output.data) - self.assertTrue('Re-generate gitolite ACLs file' in output.data) - self.assertTrue( - 'Re-generate user ssh key files' in output.data) - self.assertTrue( - '\n Gitolite ACLs updated' - in output.data) - - @patch('pagure.generate_user_key_files') - def test_admin_refresh_ssh(self, gakf): - """ Test the admin_refresh_ssh endpoint. """ - gakf.return_value = True - - output = self.app.get('/admin/ssh') - self.assertEqual(output.status_code, 404) - - output = self.app.post('/admin/ssh') - self.assertEqual(output.status_code, 302) - - user = tests.FakeUser() - with tests.user_set(pagure.APP, user): - output = self.app.post('/admin/ssh', follow_redirects=True) - self.assertEqual(output.status_code, 404) - self.assertIn( - '\n Access restricted', - output.data) - - user.username = 'foo' - with tests.user_set(pagure.APP, user): - output = self.app.post('/admin/ssh', follow_redirects=True) - self.assertEqual(output.status_code, 200) - self.assertIn( - '\n Access restricted', - output.data) - - user = tests.FakeUser( - username='pingou', - groups=pagure.APP.config['ADMIN_GROUP']) - with tests.user_set(pagure.APP, user): - output = self.app.post('/admin/ssh', follow_redirects=True) - self.assertEqual(output.status_code, 200) - self.assertTrue('

    Admin section

    ' in output.data) - self.assertTrue('Re-generate gitolite ACLs file' in output.data) - self.assertTrue( - 'Re-generate user ssh key files' in output.data) - self.assertFalse( - '
  • Authorized file updated
  • ' - in output.data) - - csrf_token = output.data.split( - 'name="csrf_token" type="hidden" value="')[1].split('">')[0] - - data = {'csrf_token': csrf_token} - output = self.app.post( - '/admin/ssh', data=data, follow_redirects=True) - self.assertEqual(output.status_code, 200) - self.assertTrue('

    Admin section

    ' in output.data) - self.assertTrue('Re-generate gitolite ACLs file' in output.data) - self.assertTrue( - 'Re-generate user ssh key files' in output.data) - self.assertTrue( - '\n User key files regenerated' - in output.data) - - def test_admin_generate_hook_token(self): - """ Test the admin_generate_hook_token endpoint. """ - - output = self.app.get('/admin/hook_token') - self.assertEqual(output.status_code, 404) - - output = self.app.post('/admin/hook_token') - self.assertEqual(output.status_code, 302) - - user = tests.FakeUser() - with tests.user_set(pagure.APP, user): - output = self.app.post('/admin/hook_token', follow_redirects=True) - self.assertEqual(output.status_code, 404) - self.assertIn( - '\n Access restricted', - output.data) - - user.username = 'foo' - with tests.user_set(pagure.APP, user): - output = self.app.post('/admin/hook_token', follow_redirects=True) - self.assertEqual(output.status_code, 200) - self.assertIn( - '\n Access restricted', - output.data) - - user = tests.FakeUser( - username='pingou', - groups=pagure.APP.config['ADMIN_GROUP']) - with tests.user_set(pagure.APP, user): - output = self.app.post('/admin/hook_token', follow_redirects=True) - self.assertEqual(output.status_code, 200) - self.assertTrue('

    Admin section

    ' in output.data) - self.assertTrue('Re-generate gitolite ACLs file' in output.data) - self.assertTrue( - 'Re-generate user ssh key files' in output.data) - self.assertTrue( - 'Re-generate hook-token for every projects' in output.data) - - csrf_token = output.data.split( - 'name="csrf_token" type="hidden" value="')[1].split('">')[0] - data = {'csrf_token': csrf_token} - - output = self.app.post( - '/admin/hook_token', data=data, follow_redirects=True) - self.assertEqual(output.status_code, 200) - self.assertTrue('

    Admin section

    ' in output.data) - self.assertTrue('Re-generate gitolite ACLs file' in output.data) - self.assertTrue( - 'Re-generate user ssh key files' in output.data) - self.assertTrue( - 'Re-generate hook-token for every projects' in output.data) - self.assertTrue( - '\n Hook token all re-generated' - in output.data) - - -if __name__ == '__main__': - SUITE = unittest.TestLoader().loadTestsFromTestCase(PagureFlaskAdmintests) - unittest.TextTestRunner(verbosity=2).run(SUITE) From f2f0f991f79157efe5cc80e8b5f8e03992b87b75 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 06 2017 20:00:34 +0000 Subject: [PATCH 4/5] Fix small typo and style in pagure/cli/admin.py --- diff --git a/pagure/cli/admin.py b/pagure/cli/admin.py index 1cbad23..c6353ce 100644 --- a/pagure/cli/admin.py +++ b/pagure/cli/admin.py @@ -36,13 +36,13 @@ def parse_arguments(): # refresh-gitolite parser_gitolite = subparsers.add_parser( 'refresh-gitolite', - help='Re-generate the gitolite confi file') + help='Re-generate the gitolite config file') parser_gitolite.set_defaults(func=do_generate_acl) # refresh-ssh parser_ssh = subparsers.add_parser( 'refresh-ssh', - help='Re-write to disk every user\'s ssh key stored in the database') + help="Re-write to disk every user's ssh key stored in the database") parser_ssh.set_defaults(func=do_refresh_ssh) # clear-hook-token From a1181c4afd8e8295fac6697cc83bb390c3466b8f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 06 2017 20:15:03 +0000 Subject: [PATCH 5/5] Use PAGURE_CONFIG or default to /etc/pagure/pagure.cfg for configuration And just be use possibility to read this file as check to allow the user to run pagure-admin --- diff --git a/pagure/cli/admin.py b/pagure/cli/admin.py index c6353ce..b292e9f 100644 --- a/pagure/cli/admin.py +++ b/pagure/cli/admin.py @@ -11,6 +11,12 @@ from __future__ import print_function import argparse import logging +import os + +if 'PAGURE_CONFIG' not in os.environ \ + and os.path.exists('/etc/pagure/pagure.cfg'): + print('Using configuration file `/etc/pagure/pagure.cfg`') + os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg' import pagure.exceptions import pagure.lib @@ -98,11 +104,6 @@ def do_generate_hook_token(): def main(): """ Start of the application. """ - # TODO: figure out if the user is allowed to run this tool at all - # -> require root? - # -> check if pagure's config file is readable? - # -> Ask for something private in pagure's config file? - # Parse the arguments args = parse_arguments()