From 0e5f4a0b34b0cefd2ba38bec559a90cca935ed28 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Sep 25 2015 23:34:04 +0000 Subject: Make gitolite pick up multiple keys per user To implement this, we write keys_/.pub, where i is an integer value relating to the line number of the specific key. Signed-off-by: Patrick Uiterwijk --- diff --git a/files/load_from_disk.py b/files/load_from_disk.py index 6837b1d..5f0feb6 100644 --- a/files/load_from_disk.py +++ b/files/load_from_disk.py @@ -57,6 +57,7 @@ def main(folder, debug=False): username=user, fullname=user, default_email='%s@fedoraproject.org' % user, + keydir=pagure.APP.config.get('GITOLITE_KEYDIR', None), ) pagure.SESSION.commit() except SQLAlchemyError, err: diff --git a/pagure/__init__.py b/pagure/__init__.py index 7aec556..d3878c6 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -160,59 +160,6 @@ def is_repo_admin(repo_obj): ) or (user in usergrps) -def generate_gitolite_acls(): - """ Generate the gitolite configuration file for all repos - """ - pagure.lib.git.write_gitolite_acls( - SESSION, APP.config['GITOLITE_CONFIG']) - - gitolite_folder = APP.config.get('GITOLITE_HOME', None) - gitolite_version = APP.config.get('GITOLITE_VERSION', 3) - if gitolite_folder: - if gitolite_version == 2: - cmd = 'GL_RC=%s GL_BINDIR=%s gl-compile-conf' % ( - APP.config.get('GL_RC'), APP.config.get('GL_BINDIR') - ) - elif gitolite_version == 3: - cmd = 'HOME=%s gitolite compile && HOME=%s gitolite trigger '\ - 'POST_COMPILE' % ( - APP.config.get('GITOLITE_HOME'), - APP.config.get('GITOLITE_HOME') - ) - else: - raise pagure.exceptions.PagureException( - 'Non-supported gitolite version "%s"' % gitolite_version - ) - subprocess.Popen( - cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=gitolite_folder - ) - # We need to do this because gitolite will also try to recreate the authorized_keys - # file, but it will ignore any keyfiles with more then a single line. So it will - # never create a authorized_keys file with more than one key for any single user. - generate_authorized_key_file() - - -def generate_gitolite_key(user, key): # pragma: no cover - """ Generate the gitolite ssh key file for the specified user - """ - gitolite_keydir = APP.config.get('GITOLITE_KEYDIR', None) - if gitolite_keydir: - keyfile = os.path.join(gitolite_keydir, '%s.pub' % user) - with open(keyfile, 'w') as stream: - # If we do more then one line, gitolite will ignore the key file. - # Symptom: WARNING: keydir/.pub does not contain exactly 1 line; ignoring - # Let us make sure we at least have the users first key in there until - # we manually recreate the authorized_keys file (should happen almost - # the same time, but to prevent issues in the most trivial case where - # a user just has a single key, we also use the gitolite system as - # fallback). - stream.write(key.split('\n')[0]) - - def generate_authorized_key_file(): # pragma: no cover """ Regenerate the `authorized_keys` file used by gitolite. """ @@ -305,7 +252,8 @@ def set_user(return_url): username=flask.g.fas_user.username, fullname=flask.g.fas_user.fullname, default_email=flask.g.fas_user.email, - ssh_key=flask.g.fas_user.get('ssh_key') + ssh_key=flask.g.fas_user.get('ssh_key'), + keydir=APP.config.get('GITOLITE_KEYDIR', None), ) SESSION.commit() except SQLAlchemyError, err: diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 3c2fcd2..7ba6349 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -152,6 +152,36 @@ def search_user(session, username=None, email=None, token=None, pattern=None): return output +def create_user_ssh_keys_on_disk(user, gitolite_keydir): + if gitolite_keydir: + # First remove any old keyfiles for the user + # Assumption: we populated the keydir. This means that files + # will be in 0/.pub, ..., and not in any deeper + # directory structures. Also, this means that if a user + # had 5 lines, they will be up to at most keys_4/.pub, + # meaning that if a user is not in keys_/.pub, with + # i being any integer, the user is most certainly not in + # keys_/.pub. + i = 0 + keyline_file = os.path.join(gitolite_keydir, + 'keys_%i' % i, + '%s.pub' % user.user) + while os.path.exists(keyline_file): + os.path.rm(keyline_file) + i += 1 + keyline_file = os.path.join(gitolite_keydir, + 'keys_%i' % i, + '%s.pub' % user.user) + + # Now let's create new keyfiles for the user + keys = user.public_ssh_key.split('\n') + for i in range(len(keys)): + keyline_dir = os.path.join(gitolite_keydir, 'keys_%i' % i) + if not os.path.exists(keyline_dir): + os.mkdir(keyline_dir) + keyfile = os.path.join(keyline_dir, '%s.pub' % user.user) + with open(keyfile, 'w') as stream: + stream.write(keys[i].strip().encode('UTF-8')) def add_issue_comment(session, issue, comment, user, ticketfolder, notify=True, redis=None): @@ -1774,7 +1804,7 @@ def get_pull_request_flag_by_uid(session, flag_uid): def set_up_user(session, username, fullname, default_email, - emails=None, ssh_key=None): + emails=None, ssh_key=None, keydir=None): ''' Set up a new user into the database or update its information. ''' user = search_user(session, username=username) if not user: @@ -1800,7 +1830,7 @@ def set_up_user(session, username, fullname, default_email, add_email_to_user(session, user, email) if ssh_key and not user.public_ssh_key: - user.public_ssh_key = ssh_key + update_user_ssh(session, user, ssh_key, keydir) return user @@ -1816,18 +1846,18 @@ def add_email_to_user(session, user, user_email): session.flush() -def update_user_ssh(session, user, ssh_key): +def update_user_ssh(session, user, ssh_key, keydir): ''' Set up a new user into the database or update its information. ''' if isinstance(user, basestring): user = __get_user(session, user) message = 'Nothing to update' - ssh_key = ssh_key.strip().replace('\n', '') \ - if ssh_key and ssh_key.strip() else None - if ssh_key != user.public_ssh_key: user.public_ssh_key = ssh_key + if keydir: + create_user_ssh_keys_on_disk(user, keydir) + pagure.lib.git.generate_gitolite_acls() session.add(user) session.flush() message = 'Public ssh key updated' diff --git a/pagure/lib/git.py b/pagure/lib/git.py index 36e6e36..1a9726c 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -120,6 +120,39 @@ def write_gitolite_acls(session, configfile): stream.write(row + '\n') +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']) + + gitolite_folder = pagure.APP.config.get('GITOLITE_HOME', None) + gitolite_version = pagure.APP.config.get('GITOLITE_VERSION', 3) + if gitolite_folder: + if gitolite_version == 2: + cmd = 'GL_RC=%s GL_BINDIR=%s gl-compile-conf' % ( + pagure.APP.config.get('GL_RC'), + pagure.APP.config.get('GL_BINDIR') + ) + elif gitolite_version == 3: + cmd = 'HOME=%s gitolite compile && HOME=%s gitolite trigger '\ + 'POST_COMPILE' % ( + pagure.APP.config.get('GITOLITE_HOME'), + pagure.APP.config.get('GITOLITE_HOME') + ) + else: + raise pagure.exceptions.PagureException( + 'Non-supported gitolite version "%s"' % gitolite_version + ) + subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=gitolite_folder + ) + + def update_git(obj, repo, repofolder, objtype='ticket'): """ Update the given issue in its git. @@ -307,6 +340,7 @@ def get_user_from_json(session, jsondata, key='user'): fullname=fullname or username, default_email=default_email, emails=useremails, + keydir=pagure.APP.config.get('GITOLITE_KEYDIR', None), ) session.commit() diff --git a/pagure/ui/admin.py b/pagure/ui/admin.py index 1b1b7b5..f9ebb02 100644 --- a/pagure/ui/admin.py +++ b/pagure/ui/admin.py @@ -16,8 +16,8 @@ from sqlalchemy.exc import SQLAlchemyError import pagure.exceptions import pagure.forms import pagure.lib +import pagure.lib.git from pagure import (APP, SESSION, - generate_gitolite_acls, generate_authorized_key_file, is_admin, admin_session_timedout) # pylint: disable=E1101 @@ -63,7 +63,7 @@ def admin_generate_acl(): form = pagure.forms.ConfirmationForm() if form.validate_on_submit(): try: - generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash('Gitolite ACLs updated') except pagure.exceptions.PagureException, err: flask.flash(str(err), 'error') diff --git a/pagure/ui/app.py b/pagure/ui/app.py index a2145e3..0de7c7c 100644 --- a/pagure/ui/app.py +++ b/pagure/ui/app.py @@ -15,11 +15,11 @@ from sqlalchemy.exc import SQLAlchemyError import pagure.exceptions import pagure.lib +import pagure.lib.git import pagure.forms import pagure.ui.filters from pagure import (APP, SESSION, cla_required, - generate_gitolite_acls, generate_gitolite_key, - generate_authorized_key_file, authenticated, + authenticated, admin_session_timedout) @@ -358,7 +358,7 @@ def new_project(): requestfolder=APP.config['REQUESTS_FOLDER'], ) SESSION.commit() - generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash(message) return flask.redirect(flask.url_for('view_repo', repo=name)) except pagure.exceptions.PagureException, err: @@ -397,9 +397,9 @@ def user_settings(): SESSION, user=user, ssh_key=ssh_key, + keydir=APP.config.get('GITOLITE_KEYDIR', None), ) if message != 'Nothing to update': - generate_gitolite_key(user.user, ssh_key) generate_authorized_key_file() SESSION.commit() flask.flash(message) diff --git a/pagure/ui/fork.py b/pagure/ui/fork.py index 8e5787b..bbfab9a 100644 --- a/pagure/ui/fork.py +++ b/pagure/ui/fork.py @@ -21,7 +21,7 @@ import pagure.lib import pagure.lib.git import pagure.forms from pagure import (APP, REDIS, SESSION, LOG, cla_required, - is_repo_admin, generate_gitolite_acls) + is_repo_admin) # pylint: disable=E1101 @@ -728,7 +728,7 @@ def fork_project(repo, username=None): user=flask.g.fas_user.username) SESSION.commit() - generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash(message) return flask.redirect( flask.url_for( diff --git a/pagure/ui/groups.py b/pagure/ui/groups.py index 2dc4bcd..7e3c5ca 100644 --- a/pagure/ui/groups.py +++ b/pagure/ui/groups.py @@ -15,6 +15,7 @@ from sqlalchemy.exc import SQLAlchemyError import pagure import pagure.forms import pagure.lib +import pagure.lib.git # pylint: disable=E1101 @@ -76,7 +77,7 @@ def view_group(group): is_admin=pagure.is_admin(), ) pagure.SESSION.commit() - pagure.generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash(msg) except pagure.exceptions.PagureException, err: pagure.SESSION.rollback() @@ -124,7 +125,7 @@ def group_user_delete(user, group): is_admin=pagure.is_admin() ) pagure.SESSION.commit() - pagure.generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash( 'User `%s` removed from the group `%s`' % (user, group)) except pagure.exceptions.PagureException, err: @@ -173,7 +174,7 @@ def group_delete(group): pagure.SESSION.delete(group_obj) pagure.SESSION.commit() - pagure.generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash( 'Group `%s` has been deleted' % (group)) diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index 6b9c4c2..ee5d548 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -1048,7 +1048,7 @@ def remove_user(repo, userid, username=None): break try: SESSION.commit() - pagure.generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash('User removed') except SQLAlchemyError as err: # pragma: no cover SESSION.rollback() @@ -1094,7 +1094,7 @@ def add_user(repo, username=None): user=flask.g.fas_user.username, ) SESSION.commit() - pagure.generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash(msg) return flask.redirect( flask.url_for( @@ -1150,7 +1150,7 @@ def add_group_project(repo, username=None): user=flask.g.fas_user.username, ) SESSION.commit() - pagure.generate_gitolite_acls() + pagure.lib.git.generate_gitolite_acls() flask.flash(msg) return flask.redirect( flask.url_for( diff --git a/tests/test.git/HEAD b/tests/test.git/HEAD new file mode 100644 index 0000000..cb089cd --- /dev/null +++ b/tests/test.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/test.git/config b/tests/test.git/config new file mode 100644 index 0000000..d5f2df9 --- /dev/null +++ b/tests/test.git/config @@ -0,0 +1,4 @@ +[core] + bare = true + repositoryformatversion = 0 + filemode = true diff --git a/tests/test.git/description b/tests/test.git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/tests/test.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/test.git/hooks/README.sample b/tests/test.git/hooks/README.sample new file mode 100755 index 0000000..d125ec8 --- /dev/null +++ b/tests/test.git/hooks/README.sample @@ -0,0 +1,5 @@ +#!/bin/sh +# +# Place appropriately named executable hook scripts into this directory +# to intercept various actions that git takes. See `git help hooks` for +# more information. diff --git a/tests/test.git/info/exclude b/tests/test.git/info/exclude new file mode 100644 index 0000000..6d05881 --- /dev/null +++ b/tests/test.git/info/exclude @@ -0,0 +1,2 @@ +# File patterns to ignore; see `git help ignore` for more information. +# Lines that start with '#' are comments. diff --git a/tests/test2.git/HEAD b/tests/test2.git/HEAD new file mode 100644 index 0000000..cb089cd --- /dev/null +++ b/tests/test2.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/test2.git/config b/tests/test2.git/config new file mode 100644 index 0000000..d5f2df9 --- /dev/null +++ b/tests/test2.git/config @@ -0,0 +1,4 @@ +[core] + bare = true + repositoryformatversion = 0 + filemode = true diff --git a/tests/test2.git/description b/tests/test2.git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/tests/test2.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/test2.git/hooks/README.sample b/tests/test2.git/hooks/README.sample new file mode 100755 index 0000000..d125ec8 --- /dev/null +++ b/tests/test2.git/hooks/README.sample @@ -0,0 +1,5 @@ +#!/bin/sh +# +# Place appropriately named executable hook scripts into this directory +# to intercept various actions that git takes. See `git help hooks` for +# more information. diff --git a/tests/test2.git/info/exclude b/tests/test2.git/info/exclude new file mode 100644 index 0000000..6d05881 --- /dev/null +++ b/tests/test2.git/info/exclude @@ -0,0 +1,2 @@ +# File patterns to ignore; see `git help ignore` for more information. +# Lines that start with '#' are comments. diff --git a/tests/test_progit_lib.py b/tests/test_progit_lib.py index fdf0b4f..0d2a172 100644 --- a/tests/test_progit_lib.py +++ b/tests/test_progit_lib.py @@ -997,7 +997,8 @@ class PagureLibtests(tests.Modeltests): session=self.session, username='skvidal', fullname='Seth', - default_email='skvidal@fp.o' + default_email='skvidal@fp.o', + keydir=pagure.APP.config.get('GITOLITE_KEYDIR', None), ) self.session.commit() @@ -1018,7 +1019,8 @@ class PagureLibtests(tests.Modeltests): session=self.session, username='skvidal', fullname='Seth V', - default_email='skvidal@fp.o' + default_email='skvidal@fp.o', + keydir=pagure.APP.config.get('GITOLITE_KEYDIR', None), ) self.session.commit() # Nothing changed @@ -1034,7 +1036,8 @@ class PagureLibtests(tests.Modeltests): session=self.session, username='skvidal', fullname='Seth', - default_email='svidal@fp.o' + default_email='svidal@fp.o', + keydir=pagure.APP.config.get('GITOLITE_KEYDIR', None), ) self.session.commit() # Email added @@ -1052,13 +1055,13 @@ class PagureLibtests(tests.Modeltests): user = pagure.lib.search_user(self.session, username='foo') self.assertEqual(user.public_ssh_key, None) - msg = pagure.lib.update_user_ssh(self.session, user, 'blah') + msg = pagure.lib.update_user_ssh(self.session, user, 'blah', keydir=None) self.assertEqual(msg, 'Public ssh key updated') - msg = pagure.lib.update_user_ssh(self.session, user, 'blah') + msg = pagure.lib.update_user_ssh(self.session, user, 'blah', keydir=None) self.assertEqual(msg, 'Nothing to update') - msg = pagure.lib.update_user_ssh(self.session, 'foo', None) + msg = pagure.lib.update_user_ssh(self.session, 'foo', None, keydir=None) self.assertEqual(msg, 'Public ssh key updated') def test_avatar_url(self):